class_name CommitmentLifecycleService extends RefCounted const EVENT_ACCEPTED := SimulationIds.EVENT_COMMITMENT_ACCEPTED const EVENT_FULFILLED := SimulationIds.EVENT_COMMITMENT_FULFILLED const EVENT_BROKEN := SimulationIds.EVENT_COMMITMENT_BROKEN const EVENT_RELEASED := SimulationIds.EVENT_COMMITMENT_RELEASED const EVENT_SUPERSEDED := SimulationIds.EVENT_COMMITMENT_SUPERSEDED const ACCEPTED_OBLIGATION := 0.25 const FULFILLED_TRUST := 0.10 const BROKEN_TRUST := -0.20 const BROKEN_HOSTILITY := 0.15 func record_acceptance( commitment: CommitmentStateRecord, event_log: RefCounted, relationships: RefCounted ) -> EconomicEventRecord: if commitment == null or not commitment.is_active() or event_log == null: return null var event := _find_fact(event_log, EVENT_ACCEPTED, commitment.get_commitment_id()) if event == null: var relationship := _get_consequence_relationship(commitment, relationships) var obligation_delta := ( minf(ACCEPTED_OBLIGATION, 1.0 - relationship.get_obligation()) if relationship != null else 0.0 ) event = _record_fact( commitment, event_log, EVENT_ACCEPTED, commitment.get_created_tick(), CommitmentStateRecord.STATUS_ACTIVE, &"accepted", CommitmentStateRecord.NO_EVENT_ID, {"obligation_delta": obligation_delta} ) if event != null: _apply_acceptance(commitment, event, relationships) return event func maintain_and_record( current_tick: int, commitments: SocialCommitmentSystem, situations: SituationSystem, event_log: RefCounted, state_facts: Dictionary, relationships: RefCounted ) -> Array[CommitmentStateRecord]: if commitments == null or situations == null or event_log == null: return [] var closed := commitments.maintain( current_tick, situations, event_log.world_events, state_facts ) for commitment in closed: record_outcome(commitment, event_log, relationships) return closed func record_outcome( commitment: CommitmentStateRecord, event_log: RefCounted, relationships: RefCounted ) -> EconomicEventRecord: if commitment == null or commitment.is_active() or event_log == null: return null var event_type := _event_type_for_status(commitment.get_status()) if event_type.is_empty(): return null var event := _find_fact(event_log, event_type, commitment.get_commitment_id()) if event == null: var acceptance := _find_fact(event_log, EVENT_ACCEPTED, commitment.get_commitment_id()) var obligation_delta := ( float(acceptance.data.get("obligation_delta", ACCEPTED_OBLIGATION)) if acceptance != null else 0.0 ) event = _record_fact( commitment, event_log, event_type, commitment.get_closed_tick(), commitment.get_status(), commitment.get_close_reason(), commitment.get_cause_event_id(), {"obligation_delta": obligation_delta} ) if event != null: _apply_outcome(commitment, event, relationships) return event func synchronize_outcomes( commitments: SocialCommitmentSystem, event_log: RefCounted, relationships: RefCounted ) -> Array[EconomicEventRecord]: var events: Array[EconomicEventRecord] = [] if commitments == null: return events for commitment in commitments.get_all_sorted(): if commitment.is_active(): continue var event := record_outcome(commitment, event_log, relationships) if event != null: events.append(event) return events func get_fact( event_log: RefCounted, event_type: StringName, commitment_id: int ) -> EconomicEventRecord: return _find_fact(event_log, event_type, commitment_id) func _record_fact( commitment: CommitmentStateRecord, event_log: RefCounted, event_type: StringName, tick: int, status: StringName, reason: StringName, cause_event_id: int, additional_payload: Dictionary = {} ) -> EconomicEventRecord: var debtor := commitment.get_debtor() var creditor := commitment.get_creditor() var debtor_actor_id := _relationship_actor_id(debtor) if not RelationshipStateRecord.is_valid_actor_id(debtor_actor_id): return null var payload := { "cause_event_id": cause_event_id, "close_reason": String(reason), "commitment_id": commitment.get_commitment_id(), "creditor": creditor.to_dictionary(), "debtor": debtor.to_dictionary(), "situation_id": commitment.get_situation_id(), "status": String(status), "terms": commitment.get_terms(), } for key in additional_payload: if payload.has(key): return null payload[key] = additional_payload[key] return event_log.record_fact( tick, event_type, debtor_actor_id, StringName("commitment_%d" % commitment.get_commitment_id()), StringName(creditor.index_key()), payload ) func _apply_acceptance( commitment: CommitmentStateRecord, event: EconomicEventRecord, relationships: RefCounted ) -> void: var relationship := _get_consequence_relationship(commitment, relationships) if relationship == null: return _apply_dimension_once( relationship, RelationshipStateRecord.DIMENSION_OBLIGATION, float(event.data.get("obligation_delta", ACCEPTED_OBLIGATION)), int(event.data["event_id"]) ) func _apply_outcome( commitment: CommitmentStateRecord, event: EconomicEventRecord, relationships: RefCounted ) -> void: var relationship := _get_consequence_relationship(commitment, relationships) if relationship == null: return var event_id := int(event.data["event_id"]) _apply_dimension_once( relationship, RelationshipStateRecord.DIMENSION_OBLIGATION, -float(event.data.get("obligation_delta", ACCEPTED_OBLIGATION)), event_id ) match commitment.get_status(): CommitmentStateRecord.STATUS_FULFILLED: _apply_dimension_once( relationship, RelationshipStateRecord.DIMENSION_TRUST, FULFILLED_TRUST, event_id ) CommitmentStateRecord.STATUS_BROKEN: _apply_dimension_once( relationship, RelationshipStateRecord.DIMENSION_TRUST, BROKEN_TRUST, event_id ) _apply_dimension_once( relationship, RelationshipStateRecord.DIMENSION_HOSTILITY, BROKEN_HOSTILITY, event_id ) static func _apply_dimension_once( relationship: RelationshipStateRecord, dimension: StringName, amount: float, cause_event_id: int ) -> void: if relationship.get_last_cause_event_id(dimension) >= cause_event_id: return relationship.adjust_dimension(dimension, amount, cause_event_id) static func _get_consequence_relationship( commitment: CommitmentStateRecord, relationships: RefCounted ) -> RelationshipStateRecord: if relationships == null: return null var observer_id := _relationship_actor_id(commitment.get_creditor()) var subject_id := _relationship_actor_id(commitment.get_debtor()) return relationships.get_or_create_relationship(observer_id, subject_id) static func _relationship_actor_id(entity: WorldEntityRef) -> int: if entity == null: return -2147483648 var entity_id := String(entity.get_entity_id()) if not entity_id.is_valid_int(): return -2147483648 var actor_id := entity_id.to_int() if entity.get_entity_type() == WorldEventRecord.ENTITY_TYPE_PLAYER: return actor_id if actor_id == SimulationIds.PLAYER_ACTOR_ID else -2147483648 if entity.get_entity_type() != WorldEventRecord.ENTITY_TYPE_NPC or actor_id < 0: return -2147483648 return actor_id static func _find_fact( event_log: RefCounted, event_type: StringName, commitment_id: int ) -> EconomicEventRecord: if event_log == null or commitment_id < 0: return null for event in event_log.get_for_type(event_type): if int(event.data.get("commitment_id", -1)) == commitment_id: return event return null static func _event_type_for_status(status: StringName) -> StringName: match status: CommitmentStateRecord.STATUS_FULFILLED: return EVENT_FULFILLED CommitmentStateRecord.STATUS_BROKEN: return EVENT_BROKEN CommitmentStateRecord.STATUS_RELEASED: return EVENT_RELEASED CommitmentStateRecord.STATUS_SUPERSEDED: return EVENT_SUPERSEDED return &""