feat: deliver emergent Jajce commitments

This commit is contained in:
Rijad Zuzo
2026-08-12 21:15:58 +02:00
parent dc0f59fcf0
commit 6b50580598
28 changed files with 2145 additions and 206 deletions
@@ -0,0 +1,250 @@
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 &""