feat: generalize social memory and relationships
This commit is contained in:
@@ -2,7 +2,10 @@ class_name EventKnowledgeSystem
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const WITNESS_RADIUS := 10.0
|
const WITNESS_RADIUS := 10.0
|
||||||
const MAX_RECENT_FACTS_PER_NPC := 3
|
const MAX_RECENT_FACTS_PER_KNOWER := 3
|
||||||
|
const MAX_RECENT_FACTS_PER_NPC := MAX_RECENT_FACTS_PER_KNOWER
|
||||||
|
const MAX_PINNED_FACTS_PER_KNOWER := 4
|
||||||
|
const COMMUNICATED_CONFIDENCE_FACTOR := 0.9
|
||||||
|
|
||||||
var known_events: Dictionary = {}
|
var known_events: Dictionary = {}
|
||||||
|
|
||||||
@@ -18,7 +21,16 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
|||||||
return learned
|
return learned
|
||||||
var event_id := int(event.data["event_id"])
|
var event_id := int(event.data["event_id"])
|
||||||
var acquired_tick := int(event.data["tick"])
|
var acquired_tick := int(event.data["tick"])
|
||||||
if actor != null:
|
if is_player_actor:
|
||||||
|
var player_record := _remember(
|
||||||
|
SimulationIds.PLAYER_ACTOR_ID,
|
||||||
|
event_id,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||||
|
acquired_tick
|
||||||
|
)
|
||||||
|
if player_record != null:
|
||||||
|
learned.append(player_record)
|
||||||
|
elif actor != null:
|
||||||
var actor_record := _remember(
|
var actor_record := _remember(
|
||||||
actor.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED, acquired_tick
|
actor.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED, acquired_tick
|
||||||
)
|
)
|
||||||
@@ -47,8 +59,11 @@ func communicate_event(
|
|||||||
) -> KnownEventStateRecord:
|
) -> KnownEventStateRecord:
|
||||||
if (
|
if (
|
||||||
event == null
|
event == null
|
||||||
|
or not KnownEventStateRecord.is_valid_actor_id(speaker_id)
|
||||||
|
or not KnownEventStateRecord.is_valid_actor_id(listener_id)
|
||||||
or speaker_id == listener_id
|
or speaker_id == listener_id
|
||||||
or listener_id == int(event.data["actor_id"])
|
or listener_id == int(event.data["actor_id"])
|
||||||
|
or acquired_tick < 0
|
||||||
or not is_knowable_event(event)
|
or not is_knowable_event(event)
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
@@ -70,10 +85,19 @@ func communicate_event(
|
|||||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||||
acquired_tick,
|
acquired_tick,
|
||||||
speaker_id,
|
speaker_id,
|
||||||
speaker_record.get_acquisition_method()
|
speaker_record.get_acquisition_method(),
|
||||||
|
speaker_record.get_hop_count() + 1,
|
||||||
|
speaker_record.get_confidence() * COMMUNICATED_CONFIDENCE_FACTOR,
|
||||||
|
speaker_record.get_salience()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func communicate_event_to_player(
|
||||||
|
event: EconomicEventRecord, speaker_id: int, acquired_tick: int
|
||||||
|
) -> KnownEventStateRecord:
|
||||||
|
return communicate_event(event, speaker_id, SimulationIds.PLAYER_ACTOR_ID, acquired_tick)
|
||||||
|
|
||||||
|
|
||||||
func knows_event(knower_id: int, event_id: int) -> bool:
|
func knows_event(knower_id: int, event_id: int) -> bool:
|
||||||
return known_events.has(_key(knower_id, event_id))
|
return known_events.has(_key(knower_id, event_id))
|
||||||
|
|
||||||
@@ -82,6 +106,37 @@ func get_record(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
|||||||
return known_events.get(_key(knower_id, event_id)) as KnownEventStateRecord
|
return known_events.get(_key(knower_id, event_id)) as KnownEventStateRecord
|
||||||
|
|
||||||
|
|
||||||
|
func pin_event(knower_id: int, event_id: int, pinned: bool = true) -> bool:
|
||||||
|
var record := get_record(knower_id, event_id)
|
||||||
|
if record == null or record.is_pinned() == pinned:
|
||||||
|
return false
|
||||||
|
if pinned and _get_pinned_count(knower_id) >= MAX_PINNED_FACTS_PER_KNOWER:
|
||||||
|
return false
|
||||||
|
return record.set_pinned(pinned)
|
||||||
|
|
||||||
|
|
||||||
|
func set_event_confidence(knower_id: int, event_id: int, confidence: float) -> bool:
|
||||||
|
var record := get_record(knower_id, event_id)
|
||||||
|
return record != null and record.set_confidence(confidence)
|
||||||
|
|
||||||
|
|
||||||
|
func set_event_salience(knower_id: int, event_id: int, salience: float) -> bool:
|
||||||
|
var record := get_record(knower_id, event_id)
|
||||||
|
return record != null and record.set_salience(salience)
|
||||||
|
|
||||||
|
|
||||||
|
func get_pinned_event_ids(knower_id: int) -> Array[int]:
|
||||||
|
var records: Array[KnownEventStateRecord] = []
|
||||||
|
for record in known_events.values():
|
||||||
|
if record.get_knower_id() == knower_id and record.is_pinned():
|
||||||
|
records.append(record)
|
||||||
|
records.sort_custom(_sort_by_acquisition)
|
||||||
|
var event_ids: Array[int] = []
|
||||||
|
for record in records:
|
||||||
|
event_ids.append(record.get_event_id())
|
||||||
|
return event_ids
|
||||||
|
|
||||||
|
|
||||||
func get_knowers(event_id: int) -> Array[int]:
|
func get_knowers(event_id: int) -> Array[int]:
|
||||||
var knower_ids: Array[int] = []
|
var knower_ids: Array[int] = []
|
||||||
for record in known_events.values():
|
for record in known_events.values():
|
||||||
@@ -146,6 +201,9 @@ func _get_ranked_event_ids(
|
|||||||
"event_id": record.get_event_id(),
|
"event_id": record.get_event_id(),
|
||||||
"acquired_tick": record.get_acquired_tick(),
|
"acquired_tick": record.get_acquired_tick(),
|
||||||
"lasting": record.get_event_id() in lasting_event_ids,
|
"lasting": record.get_event_id() in lasting_event_ids,
|
||||||
|
"pinned": record.is_pinned(),
|
||||||
|
"confidence": record.get_confidence(),
|
||||||
|
"salience": record.get_salience(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -171,7 +229,7 @@ func maintain_retention(
|
|||||||
var recent_by_knower := {}
|
var recent_by_knower := {}
|
||||||
for record in known_events.values():
|
for record in known_events.values():
|
||||||
var key := _key(record.get_knower_id(), record.get_event_id())
|
var key := _key(record.get_knower_id(), record.get_event_id())
|
||||||
if lasting_keys.has(key):
|
if lasting_keys.has(key) or record.is_pinned():
|
||||||
continue
|
continue
|
||||||
if expire_by_age and current_tick - record.get_acquired_tick() >= max_recent_age:
|
if expire_by_age and current_tick - record.get_acquired_tick() >= max_recent_age:
|
||||||
removal_keys[key] = true
|
removal_keys[key] = true
|
||||||
@@ -181,7 +239,7 @@ func maintain_retention(
|
|||||||
recent_by_knower[record.get_knower_id()] = recent
|
recent_by_knower[record.get_knower_id()] = recent
|
||||||
for recent in recent_by_knower.values():
|
for recent in recent_by_knower.values():
|
||||||
recent.sort_custom(_sort_by_acquisition)
|
recent.sort_custom(_sort_by_acquisition)
|
||||||
var excess: int = recent.size() - MAX_RECENT_FACTS_PER_NPC
|
var excess: int = recent.size() - MAX_RECENT_FACTS_PER_KNOWER
|
||||||
for index in range(maxi(excess, 0)):
|
for index in range(maxi(excess, 0)):
|
||||||
var record := recent[index] as KnownEventStateRecord
|
var record := recent[index] as KnownEventStateRecord
|
||||||
removal_keys[_key(record.get_knower_id(), record.get_event_id())] = true
|
removal_keys[_key(record.get_knower_id(), record.get_event_id())] = true
|
||||||
@@ -214,24 +272,46 @@ func _remember(
|
|||||||
event_id: int,
|
event_id: int,
|
||||||
acquisition_method: StringName,
|
acquisition_method: StringName,
|
||||||
acquired_tick: int,
|
acquired_tick: int,
|
||||||
source_npc_id: int = KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
source_actor_id: int = KnownEventStateRecord.NO_SOURCE_ACTOR_ID,
|
||||||
source_acquisition_method: StringName = &""
|
source_acquisition_method: StringName = &"",
|
||||||
|
hop_count: int = KnownEventStateRecord.AUTO_HOP_COUNT,
|
||||||
|
confidence: float = KnownEventStateRecord.DEFAULT_CONFIDENCE,
|
||||||
|
salience: float = KnownEventStateRecord.DEFAULT_SALIENCE,
|
||||||
|
pinned: bool = false
|
||||||
) -> KnownEventStateRecord:
|
) -> KnownEventStateRecord:
|
||||||
|
if not KnownEventStateRecord.is_valid_actor_id(knower_id) or event_id < 0 or acquired_tick < 0:
|
||||||
|
return null
|
||||||
var key := _key(knower_id, event_id)
|
var key := _key(knower_id, event_id)
|
||||||
if known_events.has(key):
|
if known_events.has(key):
|
||||||
return null
|
return null
|
||||||
|
var source_npc_id := (
|
||||||
|
source_actor_id if source_actor_id >= 0 else KnownEventStateRecord.NO_SOURCE_NPC_ID
|
||||||
|
)
|
||||||
var record := KnownEventStateRecord.create(
|
var record := KnownEventStateRecord.create(
|
||||||
knower_id,
|
knower_id,
|
||||||
event_id,
|
event_id,
|
||||||
acquisition_method,
|
acquisition_method,
|
||||||
source_npc_id,
|
source_npc_id,
|
||||||
acquired_tick,
|
acquired_tick,
|
||||||
source_acquisition_method
|
source_acquisition_method,
|
||||||
|
hop_count,
|
||||||
|
confidence,
|
||||||
|
salience,
|
||||||
|
source_actor_id,
|
||||||
|
pinned
|
||||||
)
|
)
|
||||||
known_events[key] = record
|
known_events[key] = record
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
func _get_pinned_count(knower_id: int) -> int:
|
||||||
|
var count := 0
|
||||||
|
for record in known_events.values():
|
||||||
|
if record.get_knower_id() == knower_id and record.is_pinned():
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
static func is_knowable_event(event: EconomicEventRecord) -> bool:
|
static func is_knowable_event(event: EconomicEventRecord) -> bool:
|
||||||
if event == null:
|
if event == null:
|
||||||
return false
|
return false
|
||||||
@@ -308,8 +388,14 @@ static func _sort_by_acquisition(
|
|||||||
|
|
||||||
|
|
||||||
static func _sort_by_importance(first: Dictionary, second: Dictionary) -> bool:
|
static func _sort_by_importance(first: Dictionary, second: Dictionary) -> bool:
|
||||||
|
if bool(first["pinned"]) != bool(second["pinned"]):
|
||||||
|
return bool(first["pinned"])
|
||||||
if bool(first["lasting"]) != bool(second["lasting"]):
|
if bool(first["lasting"]) != bool(second["lasting"]):
|
||||||
return bool(first["lasting"])
|
return bool(first["lasting"])
|
||||||
|
if not is_equal_approx(float(first["salience"]), float(second["salience"])):
|
||||||
|
return float(first["salience"]) > float(second["salience"])
|
||||||
|
if not is_equal_approx(float(first["confidence"]), float(second["confidence"])):
|
||||||
|
return float(first["confidence"]) > float(second["confidence"])
|
||||||
if int(first["acquired_tick"]) != int(second["acquired_tick"]):
|
if int(first["acquired_tick"]) != int(second["acquired_tick"]):
|
||||||
return int(first["acquired_tick"]) > int(second["acquired_tick"])
|
return int(first["acquired_tick"]) > int(second["acquired_tick"])
|
||||||
return int(first["event_id"]) > int(second["event_id"])
|
return int(first["event_id"]) > int(second["event_id"])
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const TRUSTED_HELP_THRESHOLD := 0.6
|
|||||||
const FOOD_AID_TRUST_GAIN := 0.15
|
const FOOD_AID_TRUST_GAIN := 0.15
|
||||||
const SHARED_WORK_FAMILIARITY_GAIN := 0.1
|
const SHARED_WORK_FAMILIARITY_GAIN := 0.1
|
||||||
const PLAYER_FAMILIARITY_SEED := 0.5
|
const PLAYER_FAMILIARITY_SEED := 0.5
|
||||||
|
const DEFAULT_SALIENT_RELATIONSHIP_LIMIT := 4
|
||||||
|
const MAX_BOUNDED_RELATIONSHIPS_PER_QUERY := 8
|
||||||
|
|
||||||
var relationships: Dictionary = {}
|
var relationships: Dictionary = {}
|
||||||
|
|
||||||
@@ -39,6 +41,86 @@ func get_relationship(observer_id: int, subject_id: int) -> RelationshipStateRec
|
|||||||
return relationships.get(_key(observer_id, subject_id)) as 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:
|
func is_trusted_for_help(observer_id: int, subject_id: int) -> bool:
|
||||||
var relationship := get_relationship(observer_id, subject_id)
|
var relationship := get_relationship(observer_id, subject_id)
|
||||||
return (
|
return (
|
||||||
@@ -49,8 +131,10 @@ func is_trusted_for_help(observer_id: int, subject_id: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func increase_shared_work_familiarity(first_id: int, second_id: int) -> void:
|
func increase_shared_work_familiarity(first_id: int, second_id: int) -> void:
|
||||||
var first_to_second := _get_or_create(first_id, second_id)
|
var first_to_second := get_or_create_relationship(first_id, second_id)
|
||||||
var second_to_first := _get_or_create(second_id, first_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)
|
first_to_second.increase_familiarity(SHARED_WORK_FAMILIARITY_GAIN)
|
||||||
second_to_first.increase_familiarity(SHARED_WORK_FAMILIARITY_GAIN)
|
second_to_first.increase_familiarity(SHARED_WORK_FAMILIARITY_GAIN)
|
||||||
|
|
||||||
@@ -174,15 +258,6 @@ func get_trusted_starving_subject(
|
|||||||
return effective_view.get_starving(best_relationship.get_subject_id())
|
return effective_view.get_starving(best_relationship.get_subject_id())
|
||||||
|
|
||||||
|
|
||||||
func _get_or_create(observer_id: int, subject_id: int) -> RelationshipStateRecord:
|
|
||||||
var relationship := get_relationship(observer_id, subject_id)
|
|
||||||
if relationship != null:
|
|
||||||
return relationship
|
|
||||||
relationship = RelationshipStateRecord.create(observer_id, subject_id, 0.0)
|
|
||||||
_add(relationship)
|
|
||||||
return relationship
|
|
||||||
|
|
||||||
|
|
||||||
func _add(relationship: RelationshipStateRecord) -> void:
|
func _add(relationship: RelationshipStateRecord) -> void:
|
||||||
relationships[_key(relationship.get_observer_id(), relationship.get_subject_id())] = relationship
|
relationships[_key(relationship.get_observer_id(), relationship.get_subject_id())] = relationship
|
||||||
|
|
||||||
@@ -207,3 +282,13 @@ static func _is_stronger(
|
|||||||
if not is_equal_approx(candidate.get_familiarity(), current.get_familiarity()):
|
if not is_equal_approx(candidate.get_familiarity(), current.get_familiarity()):
|
||||||
return candidate.get_familiarity() > current.get_familiarity()
|
return candidate.get_familiarity() > current.get_familiarity()
|
||||||
return candidate.get_subject_id() < current.get_subject_id()
|
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()
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
class_name KnownEventStateRecord
|
class_name KnownEventStateRecord
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_VERSION := 3
|
const SCHEMA_VERSION := 4
|
||||||
|
const LEGACY_SCHEMA_VERSION := 3
|
||||||
const NO_SOURCE_NPC_ID := -1
|
const NO_SOURCE_NPC_ID := -1
|
||||||
|
const NO_SOURCE_ACTOR_ID := -2
|
||||||
|
const DEFAULT_CONFIDENCE := 1.0
|
||||||
|
const DEFAULT_SALIENCE := 0.5
|
||||||
|
const AUTO_HOP_COUNT := -1
|
||||||
|
|
||||||
const VALID_ACQUISITION_METHODS := [
|
const VALID_ACQUISITION_METHODS := [
|
||||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||||
@@ -28,8 +33,28 @@ static func create(
|
|||||||
acquisition_method: StringName = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
acquisition_method: StringName = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||||
source_npc_id: int = NO_SOURCE_NPC_ID,
|
source_npc_id: int = NO_SOURCE_NPC_ID,
|
||||||
acquired_tick: int = 0,
|
acquired_tick: int = 0,
|
||||||
source_acquisition_method: StringName = &""
|
source_acquisition_method: StringName = &"",
|
||||||
|
hop_count: int = AUTO_HOP_COUNT,
|
||||||
|
confidence: float = DEFAULT_CONFIDENCE,
|
||||||
|
salience: float = DEFAULT_SALIENCE,
|
||||||
|
source_actor_id: int = NO_SOURCE_ACTOR_ID,
|
||||||
|
pinned: bool = false
|
||||||
) -> KnownEventStateRecord:
|
) -> KnownEventStateRecord:
|
||||||
|
var is_communicated := acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||||
|
var canonical_source_actor_id := source_actor_id
|
||||||
|
var canonical_source_npc_id := source_npc_id
|
||||||
|
if is_communicated:
|
||||||
|
if canonical_source_actor_id == NO_SOURCE_ACTOR_ID:
|
||||||
|
canonical_source_actor_id = source_npc_id
|
||||||
|
canonical_source_npc_id = (
|
||||||
|
canonical_source_actor_id if canonical_source_actor_id >= 0 else NO_SOURCE_NPC_ID
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
canonical_source_actor_id = NO_SOURCE_ACTOR_ID
|
||||||
|
canonical_source_npc_id = NO_SOURCE_NPC_ID
|
||||||
|
var canonical_hop_count := hop_count
|
||||||
|
if canonical_hop_count == AUTO_HOP_COUNT:
|
||||||
|
canonical_hop_count = 1 if is_communicated else 0
|
||||||
return (
|
return (
|
||||||
KnownEventStateRecord
|
KnownEventStateRecord
|
||||||
. new(
|
. new(
|
||||||
@@ -38,56 +63,82 @@ static func create(
|
|||||||
"knower_id": knower_id,
|
"knower_id": knower_id,
|
||||||
"event_id": event_id,
|
"event_id": event_id,
|
||||||
"acquisition_method": String(acquisition_method),
|
"acquisition_method": String(acquisition_method),
|
||||||
"source_npc_id": source_npc_id,
|
"source_npc_id": canonical_source_npc_id,
|
||||||
|
"source_actor_id": canonical_source_actor_id,
|
||||||
"acquired_tick": acquired_tick,
|
"acquired_tick": acquired_tick,
|
||||||
"source_acquisition_method": String(source_acquisition_method),
|
"source_acquisition_method": String(source_acquisition_method),
|
||||||
|
"hop_count": canonical_hop_count,
|
||||||
|
"confidence": clampf(confidence, 0.0, 1.0),
|
||||||
|
"salience": clampf(salience, 0.0, 1.0),
|
||||||
|
"pinned": pinned,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
static func from_dictionary(record_data: Dictionary) -> KnownEventStateRecord:
|
static func from_dictionary(record_data: Dictionary) -> KnownEventStateRecord:
|
||||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
var version := int(record_data.get("schema_version", -1))
|
||||||
|
var normalized := record_data.duplicate(true)
|
||||||
|
if version == LEGACY_SCHEMA_VERSION:
|
||||||
|
normalized = _migrate_v3(record_data)
|
||||||
|
elif version != SCHEMA_VERSION:
|
||||||
return null
|
return null
|
||||||
if not (
|
if not (
|
||||||
record_data
|
normalized
|
||||||
. has_all(
|
. has_all(
|
||||||
[
|
[
|
||||||
"knower_id",
|
"knower_id",
|
||||||
"event_id",
|
"event_id",
|
||||||
"acquisition_method",
|
"acquisition_method",
|
||||||
"source_npc_id",
|
"source_npc_id",
|
||||||
|
"source_actor_id",
|
||||||
"acquired_tick",
|
"acquired_tick",
|
||||||
"source_acquisition_method",
|
"source_acquisition_method",
|
||||||
|
"hop_count",
|
||||||
|
"confidence",
|
||||||
|
"salience",
|
||||||
|
"pinned",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
var knower_id := int(record_data["knower_id"])
|
var knower_id := int(normalized["knower_id"])
|
||||||
var event_id := int(record_data["event_id"])
|
var event_id := int(normalized["event_id"])
|
||||||
var acquisition_method := StringName(record_data["acquisition_method"])
|
var acquisition_method := StringName(normalized["acquisition_method"])
|
||||||
var source_npc_id := int(record_data["source_npc_id"])
|
var source_npc_id := int(normalized["source_npc_id"])
|
||||||
var acquired_tick := int(record_data["acquired_tick"])
|
var source_actor_id := int(normalized["source_actor_id"])
|
||||||
var source_acquisition_method := StringName(record_data["source_acquisition_method"])
|
var acquired_tick := int(normalized["acquired_tick"])
|
||||||
if knower_id < 0 or event_id < 0 or acquired_tick < 0:
|
var source_acquisition_method := StringName(normalized["source_acquisition_method"])
|
||||||
|
var hop_count := int(normalized["hop_count"])
|
||||||
|
var confidence := float(normalized["confidence"])
|
||||||
|
var salience := float(normalized["salience"])
|
||||||
|
if not is_valid_actor_id(knower_id) or event_id < 0 or acquired_tick < 0 or hop_count < 0:
|
||||||
|
return null
|
||||||
|
if not is_finite(confidence) or confidence < 0.0 or confidence > 1.0:
|
||||||
|
return null
|
||||||
|
if not is_finite(salience) or salience < 0.0 or salience > 1.0:
|
||||||
|
return null
|
||||||
|
if typeof(normalized["pinned"]) != TYPE_BOOL:
|
||||||
return null
|
return null
|
||||||
if acquisition_method not in VALID_ACQUISITION_METHODS:
|
if acquisition_method not in VALID_ACQUISITION_METHODS:
|
||||||
return null
|
return null
|
||||||
if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||||
if source_npc_id < 0 or source_npc_id == knower_id:
|
if not is_valid_actor_id(source_actor_id) or source_actor_id == knower_id:
|
||||||
return null
|
return null
|
||||||
if source_acquisition_method not in DIRECT_ACQUISITION_METHODS:
|
if source_actor_id >= 0 and source_npc_id != source_actor_id:
|
||||||
return null
|
return null
|
||||||
elif source_npc_id != NO_SOURCE_NPC_ID or not source_acquisition_method.is_empty():
|
if source_actor_id == SimulationIds.PLAYER_ACTOR_ID and source_npc_id != NO_SOURCE_NPC_ID:
|
||||||
|
return null
|
||||||
|
if source_acquisition_method not in DIRECT_ACQUISITION_METHODS or hop_count != 1:
|
||||||
|
return null
|
||||||
|
elif (
|
||||||
|
source_npc_id != NO_SOURCE_NPC_ID
|
||||||
|
or source_actor_id != NO_SOURCE_ACTOR_ID
|
||||||
|
or not source_acquisition_method.is_empty()
|
||||||
|
or hop_count != 0
|
||||||
|
):
|
||||||
return null
|
return null
|
||||||
return create(
|
return KnownEventStateRecord.new(normalized)
|
||||||
knower_id,
|
|
||||||
event_id,
|
|
||||||
acquisition_method,
|
|
||||||
source_npc_id,
|
|
||||||
acquired_tick,
|
|
||||||
source_acquisition_method
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func get_knower_id() -> int:
|
func get_knower_id() -> int:
|
||||||
@@ -106,6 +157,10 @@ func get_source_npc_id() -> int:
|
|||||||
return int(data["source_npc_id"])
|
return int(data["source_npc_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_source_actor_id() -> int:
|
||||||
|
return int(data["source_actor_id"])
|
||||||
|
|
||||||
|
|
||||||
func get_acquired_tick() -> int:
|
func get_acquired_tick() -> int:
|
||||||
return int(data["acquired_tick"])
|
return int(data["acquired_tick"])
|
||||||
|
|
||||||
@@ -114,5 +169,69 @@ func get_source_acquisition_method() -> StringName:
|
|||||||
return StringName(data["source_acquisition_method"])
|
return StringName(data["source_acquisition_method"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_hop_count() -> int:
|
||||||
|
return int(data["hop_count"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_confidence() -> float:
|
||||||
|
return float(data["confidence"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_salience() -> float:
|
||||||
|
return float(data["salience"])
|
||||||
|
|
||||||
|
|
||||||
|
func is_pinned() -> bool:
|
||||||
|
return bool(data["pinned"])
|
||||||
|
|
||||||
|
|
||||||
|
func set_pinned(pinned: bool) -> bool:
|
||||||
|
if is_pinned() == pinned:
|
||||||
|
return false
|
||||||
|
data["pinned"] = pinned
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func set_confidence(confidence: float) -> bool:
|
||||||
|
if not is_finite(confidence):
|
||||||
|
return false
|
||||||
|
var bounded := clampf(confidence, 0.0, 1.0)
|
||||||
|
if is_equal_approx(get_confidence(), bounded):
|
||||||
|
return false
|
||||||
|
data["confidence"] = bounded
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func set_salience(salience: float) -> bool:
|
||||||
|
if not is_finite(salience):
|
||||||
|
return false
|
||||||
|
var bounded := clampf(salience, 0.0, 1.0)
|
||||||
|
if is_equal_approx(get_salience(), bounded):
|
||||||
|
return false
|
||||||
|
data["salience"] = bounded
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
return data.duplicate(true)
|
return data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
static func is_valid_actor_id(actor_id: int) -> bool:
|
||||||
|
return actor_id >= 0 or actor_id == SimulationIds.PLAYER_ACTOR_ID
|
||||||
|
|
||||||
|
|
||||||
|
static func _migrate_v3(record_data: Dictionary) -> Dictionary:
|
||||||
|
var migrated := record_data.duplicate(true)
|
||||||
|
var acquisition_method := StringName(migrated.get("acquisition_method", &""))
|
||||||
|
var is_communicated := acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||||
|
migrated["schema_version"] = SCHEMA_VERSION
|
||||||
|
migrated["source_actor_id"] = (
|
||||||
|
int(migrated.get("source_npc_id", NO_SOURCE_NPC_ID))
|
||||||
|
if is_communicated
|
||||||
|
else NO_SOURCE_ACTOR_ID
|
||||||
|
)
|
||||||
|
migrated["hop_count"] = 1 if is_communicated else 0
|
||||||
|
migrated["confidence"] = DEFAULT_CONFIDENCE
|
||||||
|
migrated["salience"] = DEFAULT_SALIENCE
|
||||||
|
migrated["pinned"] = false
|
||||||
|
return migrated
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
class_name RelationshipStateRecord
|
class_name RelationshipStateRecord
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_VERSION := 1
|
const SCHEMA_VERSION := 2
|
||||||
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
const NO_CAUSE_EVENT := -1
|
const NO_CAUSE_EVENT := -1
|
||||||
const NEUTRAL_TRUST := 0.5
|
const NEUTRAL_TRUST := 0.5
|
||||||
|
|
||||||
|
const DIMENSION_FAMILIARITY := &"familiarity"
|
||||||
|
const DIMENSION_TRUST := &"trust"
|
||||||
|
const DIMENSION_AFFECTION := &"affection"
|
||||||
|
const DIMENSION_RESPECT := &"respect"
|
||||||
|
const DIMENSION_FEAR := &"fear"
|
||||||
|
const DIMENSION_OBLIGATION := &"obligation"
|
||||||
|
const DIMENSION_HOSTILITY := &"hostility"
|
||||||
|
const DIMENSIONS := [
|
||||||
|
DIMENSION_FAMILIARITY,
|
||||||
|
DIMENSION_TRUST,
|
||||||
|
DIMENSION_AFFECTION,
|
||||||
|
DIMENSION_RESPECT,
|
||||||
|
DIMENSION_FEAR,
|
||||||
|
DIMENSION_OBLIGATION,
|
||||||
|
DIMENSION_HOSTILITY,
|
||||||
|
]
|
||||||
|
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
|
|
||||||
|
|
||||||
@@ -28,34 +46,50 @@ static func create(
|
|||||||
"subject_id": subject_id,
|
"subject_id": subject_id,
|
||||||
"familiarity": clampf(familiarity, 0.0, 1.0),
|
"familiarity": clampf(familiarity, 0.0, 1.0),
|
||||||
"trust": clampf(trust, 0.0, 1.0),
|
"trust": clampf(trust, 0.0, 1.0),
|
||||||
|
"affection": 0.0,
|
||||||
|
"respect": 0.0,
|
||||||
|
"fear": 0.0,
|
||||||
|
"obligation": 0.0,
|
||||||
|
"hostility": 0.0,
|
||||||
|
"last_familiarity_cause_event_id": NO_CAUSE_EVENT,
|
||||||
"last_trust_cause_event_id": last_trust_cause_event_id,
|
"last_trust_cause_event_id": last_trust_cause_event_id,
|
||||||
|
"last_affection_cause_event_id": NO_CAUSE_EVENT,
|
||||||
|
"last_respect_cause_event_id": NO_CAUSE_EVENT,
|
||||||
|
"last_fear_cause_event_id": NO_CAUSE_EVENT,
|
||||||
|
"last_obligation_cause_event_id": NO_CAUSE_EVENT,
|
||||||
|
"last_hostility_cause_event_id": NO_CAUSE_EVENT,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
static func from_dictionary(record_data: Dictionary) -> RelationshipStateRecord:
|
static func from_dictionary(record_data: Dictionary) -> RelationshipStateRecord:
|
||||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
var version := int(record_data.get("schema_version", -1))
|
||||||
|
var normalized := record_data.duplicate(true)
|
||||||
|
if version == LEGACY_SCHEMA_VERSION:
|
||||||
|
normalized = _migrate_v1(record_data)
|
||||||
|
elif version != SCHEMA_VERSION:
|
||||||
return null
|
return null
|
||||||
if not record_data.has_all(
|
var required_fields := ["observer_id", "subject_id"]
|
||||||
["observer_id", "subject_id", "familiarity", "trust", "last_trust_cause_event_id"]
|
for dimension in DIMENSIONS:
|
||||||
):
|
required_fields.append(String(dimension))
|
||||||
|
required_fields.append(_cause_key(dimension))
|
||||||
|
if not normalized.has_all(required_fields):
|
||||||
return null
|
return null
|
||||||
var observer_id := int(record_data["observer_id"])
|
var observer_id := int(normalized["observer_id"])
|
||||||
var subject_id := int(record_data["subject_id"])
|
var subject_id := int(normalized["subject_id"])
|
||||||
var familiarity := float(record_data["familiarity"])
|
if not is_valid_actor_id(observer_id) or not is_valid_actor_id(subject_id):
|
||||||
var trust := float(record_data["trust"])
|
|
||||||
var cause_event_id := int(record_data["last_trust_cause_event_id"])
|
|
||||||
var is_player_subject := subject_id == SimulationIds.PLAYER_ACTOR_ID
|
|
||||||
if observer_id < 0 or observer_id == subject_id:
|
|
||||||
return null
|
return null
|
||||||
if not is_player_subject and subject_id < 0:
|
if observer_id == subject_id:
|
||||||
return null
|
return null
|
||||||
if familiarity < 0.0 or familiarity > 1.0 or trust < 0.0 or trust > 1.0:
|
for dimension in DIMENSIONS:
|
||||||
return null
|
var value := float(normalized[String(dimension)])
|
||||||
if cause_event_id < NO_CAUSE_EVENT:
|
var cause_event_id := int(normalized[_cause_key(dimension)])
|
||||||
return null
|
if not is_finite(value) or value < 0.0 or value > 1.0:
|
||||||
return create(observer_id, subject_id, familiarity, trust, cause_event_id)
|
return null
|
||||||
|
if cause_event_id < NO_CAUSE_EVENT:
|
||||||
|
return null
|
||||||
|
return RelationshipStateRecord.new(normalized)
|
||||||
|
|
||||||
|
|
||||||
func get_observer_id() -> int:
|
func get_observer_id() -> int:
|
||||||
@@ -67,31 +101,162 @@ func get_subject_id() -> int:
|
|||||||
|
|
||||||
|
|
||||||
func get_familiarity() -> float:
|
func get_familiarity() -> float:
|
||||||
return float(data["familiarity"])
|
return get_dimension(DIMENSION_FAMILIARITY)
|
||||||
|
|
||||||
|
|
||||||
func get_trust() -> float:
|
func get_trust() -> float:
|
||||||
return float(data["trust"])
|
return get_dimension(DIMENSION_TRUST)
|
||||||
|
|
||||||
|
|
||||||
|
func get_affection() -> float:
|
||||||
|
return get_dimension(DIMENSION_AFFECTION)
|
||||||
|
|
||||||
|
|
||||||
|
func get_respect() -> float:
|
||||||
|
return get_dimension(DIMENSION_RESPECT)
|
||||||
|
|
||||||
|
|
||||||
|
func get_fear() -> float:
|
||||||
|
return get_dimension(DIMENSION_FEAR)
|
||||||
|
|
||||||
|
|
||||||
|
func get_obligation() -> float:
|
||||||
|
return get_dimension(DIMENSION_OBLIGATION)
|
||||||
|
|
||||||
|
|
||||||
|
func get_hostility() -> float:
|
||||||
|
return get_dimension(DIMENSION_HOSTILITY)
|
||||||
|
|
||||||
|
|
||||||
|
func get_dimension(dimension: StringName) -> float:
|
||||||
|
if not has_dimension(dimension):
|
||||||
|
return -1.0
|
||||||
|
return float(data[String(dimension)])
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_familiarity_cause_event_id() -> int:
|
||||||
|
return get_last_cause_event_id(DIMENSION_FAMILIARITY)
|
||||||
|
|
||||||
|
|
||||||
func get_last_trust_cause_event_id() -> int:
|
func get_last_trust_cause_event_id() -> int:
|
||||||
return int(data["last_trust_cause_event_id"])
|
return get_last_cause_event_id(DIMENSION_TRUST)
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_affection_cause_event_id() -> int:
|
||||||
|
return get_last_cause_event_id(DIMENSION_AFFECTION)
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_respect_cause_event_id() -> int:
|
||||||
|
return get_last_cause_event_id(DIMENSION_RESPECT)
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_fear_cause_event_id() -> int:
|
||||||
|
return get_last_cause_event_id(DIMENSION_FEAR)
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_obligation_cause_event_id() -> int:
|
||||||
|
return get_last_cause_event_id(DIMENSION_OBLIGATION)
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_hostility_cause_event_id() -> int:
|
||||||
|
return get_last_cause_event_id(DIMENSION_HOSTILITY)
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_cause_event_id(dimension: StringName) -> int:
|
||||||
|
if not has_dimension(dimension):
|
||||||
|
return NO_CAUSE_EVENT
|
||||||
|
return int(data[_cause_key(dimension)])
|
||||||
|
|
||||||
|
|
||||||
func increase_familiarity(amount: float) -> float:
|
func increase_familiarity(amount: float) -> float:
|
||||||
var previous := get_familiarity()
|
return adjust_dimension(DIMENSION_FAMILIARITY, maxf(amount, 0.0), NO_CAUSE_EVENT)
|
||||||
data["familiarity"] = clampf(previous + maxf(amount, 0.0), 0.0, 1.0)
|
|
||||||
return get_familiarity() - previous
|
|
||||||
|
func increase_familiarity_with_cause(amount: float, cause_event_id: int) -> float:
|
||||||
|
return adjust_dimension(DIMENSION_FAMILIARITY, maxf(amount, 0.0), cause_event_id)
|
||||||
|
|
||||||
|
|
||||||
func increase_trust(amount: float, cause_event_id: int) -> float:
|
func increase_trust(amount: float, cause_event_id: int) -> float:
|
||||||
var previous := get_trust()
|
return adjust_dimension(DIMENSION_TRUST, maxf(amount, 0.0), cause_event_id)
|
||||||
data["trust"] = clampf(previous + maxf(amount, 0.0), 0.0, 1.0)
|
|
||||||
var applied := get_trust() - previous
|
|
||||||
if applied > 0.0:
|
func adjust_affection(amount: float, cause_event_id: int) -> float:
|
||||||
data["last_trust_cause_event_id"] = cause_event_id
|
return adjust_dimension(DIMENSION_AFFECTION, amount, cause_event_id)
|
||||||
|
|
||||||
|
|
||||||
|
func adjust_respect(amount: float, cause_event_id: int) -> float:
|
||||||
|
return adjust_dimension(DIMENSION_RESPECT, amount, cause_event_id)
|
||||||
|
|
||||||
|
|
||||||
|
func adjust_fear(amount: float, cause_event_id: int) -> float:
|
||||||
|
return adjust_dimension(DIMENSION_FEAR, amount, cause_event_id)
|
||||||
|
|
||||||
|
|
||||||
|
func adjust_obligation(amount: float, cause_event_id: int) -> float:
|
||||||
|
return adjust_dimension(DIMENSION_OBLIGATION, amount, cause_event_id)
|
||||||
|
|
||||||
|
|
||||||
|
func adjust_hostility(amount: float, cause_event_id: int) -> float:
|
||||||
|
return adjust_dimension(DIMENSION_HOSTILITY, amount, cause_event_id)
|
||||||
|
|
||||||
|
|
||||||
|
func adjust_dimension(dimension: StringName, amount: float, cause_event_id: int) -> float:
|
||||||
|
if not has_dimension(dimension) or not is_finite(amount) or cause_event_id < NO_CAUSE_EVENT:
|
||||||
|
return 0.0
|
||||||
|
var previous := get_dimension(dimension)
|
||||||
|
var current := clampf(previous + amount, 0.0, 1.0)
|
||||||
|
var applied := current - previous
|
||||||
|
if is_zero_approx(applied):
|
||||||
|
return 0.0
|
||||||
|
data[String(dimension)] = current
|
||||||
|
if cause_event_id != NO_CAUSE_EVENT:
|
||||||
|
data[_cause_key(dimension)] = cause_event_id
|
||||||
return applied
|
return applied
|
||||||
|
|
||||||
|
|
||||||
|
func get_salience() -> float:
|
||||||
|
var salience := get_familiarity()
|
||||||
|
salience = maxf(salience, absf(get_trust() - NEUTRAL_TRUST) * 2.0)
|
||||||
|
for dimension in [
|
||||||
|
DIMENSION_AFFECTION,
|
||||||
|
DIMENSION_RESPECT,
|
||||||
|
DIMENSION_FEAR,
|
||||||
|
DIMENSION_OBLIGATION,
|
||||||
|
DIMENSION_HOSTILITY,
|
||||||
|
]:
|
||||||
|
salience = maxf(salience, get_dimension(dimension))
|
||||||
|
return clampf(salience, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
return data.duplicate(true)
|
return data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
static func has_dimension(dimension: StringName) -> bool:
|
||||||
|
return dimension in DIMENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
static func is_valid_actor_id(actor_id: int) -> bool:
|
||||||
|
return actor_id >= 0 or actor_id == SimulationIds.PLAYER_ACTOR_ID
|
||||||
|
|
||||||
|
|
||||||
|
static func _cause_key(dimension: StringName) -> String:
|
||||||
|
return "last_%s_cause_event_id" % String(dimension)
|
||||||
|
|
||||||
|
|
||||||
|
static func _migrate_v1(record_data: Dictionary) -> Dictionary:
|
||||||
|
var migrated := record_data.duplicate(true)
|
||||||
|
migrated["schema_version"] = SCHEMA_VERSION
|
||||||
|
for dimension in [
|
||||||
|
DIMENSION_AFFECTION,
|
||||||
|
DIMENSION_RESPECT,
|
||||||
|
DIMENSION_FEAR,
|
||||||
|
DIMENSION_OBLIGATION,
|
||||||
|
DIMENSION_HOSTILITY,
|
||||||
|
]:
|
||||||
|
migrated[String(dimension)] = 0.0
|
||||||
|
for dimension in DIMENSIONS:
|
||||||
|
var cause_key := _cause_key(dimension)
|
||||||
|
if not migrated.has(cause_key):
|
||||||
|
migrated[cause_key] = NO_CAUSE_EVENT
|
||||||
|
return migrated
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
extends GutTest
|
||||||
|
|
||||||
|
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
||||||
|
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func test_known_event_v3_migrates_metadata_and_player_is_a_valid_knower() -> void:
|
||||||
|
var legacy_data := {
|
||||||
|
"schema_version": KnownEventStateRecord.LEGACY_SCHEMA_VERSION,
|
||||||
|
"knower_id": 2,
|
||||||
|
"event_id": 12,
|
||||||
|
"acquisition_method": String(SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED),
|
||||||
|
"source_npc_id": 1,
|
||||||
|
"acquired_tick": 30,
|
||||||
|
"source_acquisition_method": String(SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED),
|
||||||
|
}
|
||||||
|
var migrated := KnownEventStateRecord.from_dictionary(legacy_data)
|
||||||
|
|
||||||
|
assert_not_null(migrated)
|
||||||
|
assert_eq(int(migrated.to_dictionary()["schema_version"]), KnownEventStateRecord.SCHEMA_VERSION)
|
||||||
|
assert_eq(migrated.get_source_actor_id(), 1)
|
||||||
|
assert_eq(migrated.get_hop_count(), 1)
|
||||||
|
assert_eq(migrated.get_confidence(), KnownEventStateRecord.DEFAULT_CONFIDENCE)
|
||||||
|
assert_eq(migrated.get_salience(), KnownEventStateRecord.DEFAULT_SALIENCE)
|
||||||
|
assert_false(migrated.is_pinned())
|
||||||
|
|
||||||
|
var player_record := KnownEventStateRecord.create(
|
||||||
|
SimulationIds.PLAYER_ACTOR_ID,
|
||||||
|
13,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||||
|
KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||||
|
31
|
||||||
|
)
|
||||||
|
assert_not_null(KnownEventStateRecord.from_dictionary(player_record.to_dictionary()))
|
||||||
|
var invalid_knower_data := player_record.to_dictionary()
|
||||||
|
invalid_knower_data["knower_id"] = KnownEventStateRecord.NO_SOURCE_ACTOR_ID
|
||||||
|
assert_null(KnownEventStateRecord.from_dictionary(invalid_knower_data))
|
||||||
|
|
||||||
|
|
||||||
|
func test_direct_npc_knowledge_can_be_communicated_explicitly_to_player() -> void:
|
||||||
|
var event := EconomicEventRecord.create(
|
||||||
|
21,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
40,
|
||||||
|
0,
|
||||||
|
SimulationIds.npc_inventory_id(0),
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
2.0,
|
||||||
|
Vector3.ZERO
|
||||||
|
)
|
||||||
|
var speaker_record := KnownEventStateRecord.create(
|
||||||
|
1,
|
||||||
|
int(event.data["event_id"]),
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||||
|
KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||||
|
40
|
||||||
|
)
|
||||||
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
||||||
|
var records: Array[KnownEventStateRecord] = [speaker_record]
|
||||||
|
knowledge_system.restore(records)
|
||||||
|
|
||||||
|
assert_false(
|
||||||
|
knowledge_system.knows_event(SimulationIds.PLAYER_ACTOR_ID, int(event.data["event_id"]))
|
||||||
|
)
|
||||||
|
var communicated := knowledge_system.communicate_event_to_player(event, 1, 45)
|
||||||
|
assert_not_null(communicated)
|
||||||
|
assert_eq(communicated.get_knower_id(), SimulationIds.PLAYER_ACTOR_ID)
|
||||||
|
assert_eq(communicated.get_source_actor_id(), 1)
|
||||||
|
assert_eq(communicated.get_source_npc_id(), 1)
|
||||||
|
assert_eq(communicated.get_hop_count(), 1)
|
||||||
|
assert_true(is_equal_approx(communicated.get_confidence(), 0.9))
|
||||||
|
assert_eq(communicated.get_source_acquisition_method(), speaker_record.get_acquisition_method())
|
||||||
|
assert_null(knowledge_system.communicate_event(event, SimulationIds.PLAYER_ACTOR_ID, 2, 46))
|
||||||
|
|
||||||
|
|
||||||
|
func test_player_performed_world_event_is_known_without_a_witness() -> void:
|
||||||
|
var event := EconomicEventRecord.create(
|
||||||
|
22,
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
41,
|
||||||
|
SimulationIds.PLAYER_ACTOR_ID,
|
||||||
|
SimulationIds.PLAYER_INVENTORY_ID,
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
1.0,
|
||||||
|
Vector3(1000.0, 0.0, 1000.0)
|
||||||
|
)
|
||||||
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
||||||
|
var learned := knowledge_system.observe_event(event, [])
|
||||||
|
|
||||||
|
assert_eq(learned.size(), 1)
|
||||||
|
assert_eq(learned[0].get_knower_id(), SimulationIds.PLAYER_ACTOR_ID)
|
||||||
|
assert_eq(learned[0].get_acquisition_method(), SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||||
|
|
||||||
|
|
||||||
|
func test_pinned_knowledge_is_bounded_and_survives_ordinary_retention() -> void:
|
||||||
|
var knowledge_system := EventKnowledgeSystemScript.new()
|
||||||
|
var records: Array[KnownEventStateRecord] = []
|
||||||
|
for event_id in range(1, 6):
|
||||||
|
records.append(
|
||||||
|
KnownEventStateRecord.create(
|
||||||
|
0,
|
||||||
|
event_id,
|
||||||
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||||
|
KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
knowledge_system.restore(records)
|
||||||
|
|
||||||
|
assert_true(knowledge_system.pin_event(0, 1))
|
||||||
|
var no_lasting: Array[KnownEventStateRecord] = []
|
||||||
|
var forgotten := knowledge_system.maintain_retention(5, 100, no_lasting, false)
|
||||||
|
assert_eq(forgotten.size(), 1)
|
||||||
|
assert_eq(forgotten[0].get_event_id(), 2)
|
||||||
|
assert_eq(knowledge_system.get_pinned_event_ids(0), [1])
|
||||||
|
assert_eq(knowledge_system.get_retained_event_ids(0, [], 0), [1, 5, 4, 3])
|
||||||
|
assert_true(knowledge_system.set_event_salience(0, 3, 2.0))
|
||||||
|
assert_eq(knowledge_system.get_record(0, 3).get_salience(), 1.0)
|
||||||
|
assert_true(knowledge_system.set_event_confidence(0, 3, -1.0))
|
||||||
|
assert_eq(knowledge_system.get_record(0, 3).get_confidence(), 0.0)
|
||||||
|
|
||||||
|
var pin_limit_system := EventKnowledgeSystemScript.new()
|
||||||
|
var pin_limit_records: Array[KnownEventStateRecord] = []
|
||||||
|
for event_id in range(1, 6):
|
||||||
|
pin_limit_records.append(KnownEventStateRecord.create(0, event_id))
|
||||||
|
pin_limit_system.restore(pin_limit_records)
|
||||||
|
for event_id in range(1, EventKnowledgeSystemScript.MAX_PINNED_FACTS_PER_KNOWER + 1):
|
||||||
|
assert_true(pin_limit_system.pin_event(0, event_id))
|
||||||
|
assert_false(
|
||||||
|
pin_limit_system.pin_event(0, EventKnowledgeSystemScript.MAX_PINNED_FACTS_PER_KNOWER + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func test_relationship_v1_migrates_all_dimensions_and_exact_causes() -> void:
|
||||||
|
var legacy_data := {
|
||||||
|
"schema_version": RelationshipStateRecord.LEGACY_SCHEMA_VERSION,
|
||||||
|
"observer_id": 1,
|
||||||
|
"subject_id": 2,
|
||||||
|
"familiarity": 0.4,
|
||||||
|
"trust": 0.6,
|
||||||
|
"last_trust_cause_event_id": 8,
|
||||||
|
}
|
||||||
|
var relationship := RelationshipStateRecord.from_dictionary(legacy_data)
|
||||||
|
|
||||||
|
assert_not_null(relationship)
|
||||||
|
assert_eq(
|
||||||
|
int(relationship.to_dictionary()["schema_version"]), RelationshipStateRecord.SCHEMA_VERSION
|
||||||
|
)
|
||||||
|
assert_eq(relationship.get_affection(), 0.0)
|
||||||
|
assert_eq(relationship.get_respect(), 0.0)
|
||||||
|
assert_eq(relationship.get_fear(), 0.0)
|
||||||
|
assert_eq(relationship.get_obligation(), 0.0)
|
||||||
|
assert_eq(relationship.get_hostility(), 0.0)
|
||||||
|
assert_eq(relationship.get_last_trust_cause_event_id(), 8)
|
||||||
|
|
||||||
|
for index in range(RelationshipStateRecord.DIMENSIONS.size()):
|
||||||
|
var dimension: StringName = RelationshipStateRecord.DIMENSIONS[index]
|
||||||
|
var cause_event_id := 20 + index
|
||||||
|
assert_true(relationship.adjust_dimension(dimension, 0.1, cause_event_id) > 0.0)
|
||||||
|
assert_eq(relationship.get_last_cause_event_id(dimension), cause_event_id)
|
||||||
|
assert_true(relationship.adjust_hostility(2.0, 99) > 0.0)
|
||||||
|
assert_eq(relationship.get_hostility(), 1.0)
|
||||||
|
assert_eq(relationship.get_last_hostility_cause_event_id(), 99)
|
||||||
|
assert_not_null(RelationshipStateRecord.from_dictionary(relationship.to_dictionary()))
|
||||||
|
|
||||||
|
var player_observer := RelationshipStateRecord.create(SimulationIds.PLAYER_ACTOR_ID, 3, 0.2)
|
||||||
|
assert_not_null(RelationshipStateRecord.from_dictionary(player_observer.to_dictionary()))
|
||||||
|
|
||||||
|
|
||||||
|
func test_sparse_relationship_queries_are_deterministic_and_bounded() -> void:
|
||||||
|
var relationship_system := RelationshipSystemScript.new()
|
||||||
|
for subject_id in range(10):
|
||||||
|
var relationship := relationship_system.get_or_create_relationship(
|
||||||
|
SimulationIds.PLAYER_ACTOR_ID, subject_id, float(subject_id) / 10.0
|
||||||
|
)
|
||||||
|
assert_not_null(relationship)
|
||||||
|
assert_true(
|
||||||
|
(
|
||||||
|
relationship_system.adjust_relationship_dimension(
|
||||||
|
SimulationIds.PLAYER_ACTOR_ID, 0, RelationshipStateRecord.DIMENSION_RESPECT, 1.0, 77
|
||||||
|
)
|
||||||
|
> 0.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var sorted := relationship_system.get_relationships_for_observer(SimulationIds.PLAYER_ACTOR_ID)
|
||||||
|
assert_eq(sorted.size(), 10)
|
||||||
|
assert_eq(sorted[0].get_subject_id(), 0)
|
||||||
|
assert_eq(sorted[9].get_subject_id(), 9)
|
||||||
|
var salient := relationship_system.get_salient_relationships(SimulationIds.PLAYER_ACTOR_ID, 100)
|
||||||
|
assert_eq(salient.size(), RelationshipSystemScript.MAX_BOUNDED_RELATIONSHIPS_PER_QUERY)
|
||||||
|
assert_eq(salient[0].get_subject_id(), 0)
|
||||||
|
assert_eq(
|
||||||
|
relationship_system.get_relationship_cause_event_id(
|
||||||
|
SimulationIds.PLAYER_ACTOR_ID, 0, RelationshipStateRecord.DIMENSION_RESPECT
|
||||||
|
),
|
||||||
|
77
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user