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 &""
@@ -0,0 +1 @@
uid://rhqx2yahw4rj
+24 -2
View File
@@ -17,12 +17,13 @@ func add_situation(
or definition == null
or not situation.is_active()
or situation.get_definition_id() != definition.situation_definition_id
or definition.get_alternative(alternative_id) == null
or (not alternative_id.is_empty() and definition.get_alternative(alternative_id) == null)
or current_tick < situation.get_created_tick()
or _entry_id_by_situation.has(situation.get_situation_id())
or not situation.select_alternative(alternative_id)
):
return null
if not alternative_id.is_empty() and not situation.select_alternative(alternative_id):
return null
var entry := QuestJournalEntryStateRecord.create(
_next_entry_id,
situation.get_situation_id(),
@@ -36,6 +37,23 @@ func add_situation(
return entry
func select_alternative(
situation: SituationStateRecord, definition: SituationDefinition, alternative_id: StringName
) -> bool:
if (
situation == null
or definition == null
or alternative_id.is_empty()
or situation.get_definition_id() != definition.situation_definition_id
or definition.get_alternative(alternative_id) == null
):
return false
var entry := get_for_situation(situation.get_situation_id())
if entry == null or not entry.is_active():
return false
return situation.select_alternative(alternative_id) and entry.select_alternative(alternative_id)
func synchronize(
situation: SituationStateRecord, current_tick: int
) -> QuestJournalEntryStateRecord:
@@ -115,3 +133,7 @@ func get_active_sorted() -> Array[QuestJournalEntryStateRecord]:
if record.is_active():
records.append(record)
return records
func get_next_entry_id() -> int:
return _next_entry_id
+4
View File
@@ -314,6 +314,10 @@ func get_active_sorted() -> Array[SituationStateRecord]:
return active
func get_next_situation_id() -> int:
return _next_situation_id
func _get_definitions_sorted() -> Array[SituationDefinition]:
var definitions: Array[SituationDefinition] = []
for definition in _definitions_by_id.values():
@@ -2,6 +2,7 @@ class_name SocialCommitmentSystem
extends RefCounted
const REASON_FACT_FULFILLED := &"world_fact_fulfilled"
const REASON_FACT_RESOLVED_BY_OTHER := &"world_fact_resolved_by_other"
const REASON_DEADLINE_MISSED := &"deadline_missed"
const REASON_SITUATION_EXPIRED := &"situation_expired"
const REASON_SITUATION_SUPERSEDED := &"situation_superseded"
@@ -70,11 +71,28 @@ func maintain(
situation, world_events, state_facts, situation.get_selected_alternative_id()
)
if progress.resolved:
var resolution_event := (
world_events.get_by_id(progress.matched_event_id) if world_events != null else null
)
var resolver := (
resolution_event.get_participant(WorldEventRecord.ROLE_ACTOR)
if resolution_event != null
else null
)
var debtor_resolved := resolver != null and resolver.equals(commitment.get_debtor())
var resolution_status := (
CommitmentStateRecord.STATUS_FULFILLED
if debtor_resolved
else CommitmentStateRecord.STATUS_SUPERSEDED
)
var resolution_reason := (
REASON_FACT_FULFILLED if debtor_resolved else REASON_FACT_RESOLVED_BY_OTHER
)
if _close(
commitment,
CommitmentStateRecord.STATUS_FULFILLED,
resolution_status,
current_tick,
REASON_FACT_FULFILLED,
resolution_reason,
progress.matched_event_id
):
closed.append(commitment)
@@ -98,7 +116,7 @@ func maintain(
closed.append(commitment)
continue
var deadline := commitment.get_deadline_tick()
if deadline >= 0 and current_tick > deadline:
if deadline >= 0 and current_tick >= deadline:
if _close(
commitment,
CommitmentStateRecord.STATUS_BROKEN,
@@ -164,6 +182,10 @@ func get_active_sorted() -> Array[CommitmentStateRecord]:
return records
func get_next_commitment_id() -> int:
return _next_commitment_id
func _close(
commitment: CommitmentStateRecord,
status: StringName,