feat: deliver emergent Jajce commitments
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user