Files
gamedev-the-steward/simulation/knowledge/EventKnowledgeSystem.gd
T

308 lines
9.4 KiB
GDScript

class_name EventKnowledgeSystem
extends RefCounted
const WITNESS_RADIUS := 10.0
const MAX_RECENT_FACTS_PER_NPC := 3
var known_events: Dictionary = {}
func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[KnownEventStateRecord]:
var learned: Array[KnownEventStateRecord] = []
if not is_knowable_event(event):
return learned
var actor_id := int(event.data["actor_id"])
var is_player_actor := actor_id == SimulationIds.PLAYER_ACTOR_ID
var actor := null if is_player_actor else _find_npc(actor_id, npcs)
if actor == null and not is_player_actor:
return learned
var event_id := int(event.data["event_id"])
var acquired_tick := int(event.data["tick"])
if actor != null:
var actor_record := _remember(
actor.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED, acquired_tick
)
if actor_record != null:
learned.append(actor_record)
var witness_radius_squared := WITNESS_RADIUS * WITNESS_RADIUS
var event_position := event.get_world_position()
for npc in npcs:
if npc.is_dead:
continue
if not is_player_actor and npc.id == actor.id:
continue
if npc.position.distance_squared_to(event_position) > witness_radius_squared:
continue
var witness_record := _remember(
npc.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, acquired_tick
)
if witness_record != null:
learned.append(witness_record)
learned.sort_custom(_sort_records)
return learned
func communicate_event(
event: EconomicEventRecord, speaker_id: int, listener_id: int, acquired_tick: int
) -> KnownEventStateRecord:
if (
event == null
or speaker_id == listener_id
or listener_id == int(event.data["actor_id"])
or not is_knowable_event(event)
):
return null
var event_id := int(event.data["event_id"])
var speaker_record := get_record(speaker_id, event_id)
if speaker_record == null:
return null
if (
speaker_record.get_acquisition_method()
not in [
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
]
):
return null
return _remember(
listener_id,
event_id,
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
acquired_tick,
speaker_id,
speaker_record.get_acquisition_method()
)
func knows_event(knower_id: int, event_id: int) -> bool:
return known_events.has(_key(knower_id, event_id))
func get_record(knower_id: int, event_id: int) -> KnownEventStateRecord:
return known_events.get(_key(knower_id, event_id)) as KnownEventStateRecord
func get_knowers(event_id: int) -> Array[int]:
var knower_ids: Array[int] = []
for record in known_events.values():
if record.get_event_id() == event_id:
knower_ids.append(record.get_knower_id())
knower_ids.sort()
return knower_ids
func get_known_event_ids(knower_id: int, max_count: int = 3) -> Array[int]:
var records: Array[KnownEventStateRecord] = []
for record in known_events.values():
if record.get_knower_id() == knower_id:
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())
if max_count <= 0 or event_ids.size() <= max_count:
return event_ids
var recent_ids: Array[int] = []
for index in range(event_ids.size() - max_count, event_ids.size()):
recent_ids.append(event_ids[index])
return recent_ids
func get_retained_event_ids(
knower_id: int, lasting_event_ids: Array[int], max_count: int = 4
) -> Array[int]:
return _get_ranked_event_ids(knower_id, lasting_event_ids, false, max_count)
func get_communicable_event_ids(
knower_id: int, lasting_event_ids: Array[int], preferred_event_id: int = -1
) -> Array[int]:
var event_ids := _get_ranked_event_ids(knower_id, lasting_event_ids, true, 0)
if preferred_event_id in event_ids:
event_ids.erase(preferred_event_id)
event_ids.push_front(preferred_event_id)
return event_ids
func _get_ranked_event_ids(
knower_id: int, lasting_event_ids: Array[int], direct_only: bool, max_count: int
) -> Array[int]:
var ranked: Array[Dictionary] = []
for record in known_events.values():
if record.get_knower_id() != knower_id:
continue
if (
direct_only
and (
record.get_acquisition_method()
not in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS
)
):
continue
(
ranked
. append(
{
"event_id": record.get_event_id(),
"acquired_tick": record.get_acquired_tick(),
"lasting": record.get_event_id() in lasting_event_ids,
}
)
)
ranked.sort_custom(_sort_by_importance)
var event_ids: Array[int] = []
for item in ranked:
event_ids.append(int(item["event_id"]))
if max_count > 0 and event_ids.size() >= max_count:
break
return event_ids
func maintain_retention(
current_tick: int,
max_recent_age: int,
lasting_records: Array[KnownEventStateRecord],
expire_by_age: bool
) -> Array[KnownEventStateRecord]:
var lasting_keys := {}
for record in lasting_records:
lasting_keys[_key(record.get_knower_id(), record.get_event_id())] = true
var removal_keys := {}
var recent_by_knower := {}
for record in known_events.values():
var key := _key(record.get_knower_id(), record.get_event_id())
if lasting_keys.has(key):
continue
if expire_by_age and current_tick - record.get_acquired_tick() >= max_recent_age:
removal_keys[key] = true
continue
var recent: Array = recent_by_knower.get(record.get_knower_id(), [])
recent.append(record)
recent_by_knower[record.get_knower_id()] = recent
for recent in recent_by_knower.values():
recent.sort_custom(_sort_by_acquisition)
var excess: int = recent.size() - MAX_RECENT_FACTS_PER_NPC
for index in range(maxi(excess, 0)):
var record := recent[index] as KnownEventStateRecord
removal_keys[_key(record.get_knower_id(), record.get_event_id())] = true
var forgotten: Array[KnownEventStateRecord] = []
for record in get_all_sorted():
var key := _key(record.get_knower_id(), record.get_event_id())
if not removal_keys.has(key):
continue
known_events.erase(key)
forgotten.append(record)
return forgotten
func get_all_sorted() -> Array[KnownEventStateRecord]:
var records: Array[KnownEventStateRecord] = []
for record in known_events.values():
records.append(record)
records.sort_custom(_sort_records)
return records
func restore(records: Array[KnownEventStateRecord]) -> void:
known_events.clear()
for record in records:
known_events[_key(record.get_knower_id(), record.get_event_id())] = record
func _remember(
knower_id: int,
event_id: int,
acquisition_method: StringName,
acquired_tick: int,
source_npc_id: int = KnownEventStateRecord.NO_SOURCE_NPC_ID,
source_acquisition_method: StringName = &""
) -> KnownEventStateRecord:
var key := _key(knower_id, event_id)
if known_events.has(key):
return null
var record := KnownEventStateRecord.create(
knower_id,
event_id,
acquisition_method,
source_npc_id,
acquired_tick,
source_acquisition_method
)
known_events[key] = record
return record
static func is_knowable_event(event: EconomicEventRecord) -> bool:
if event == null:
return false
var event_type := StringName(event.data["event_type"])
var item_id := StringName(event.data["item_id"])
if event_type == SimulationIds.EVENT_TASK_BLOCKED:
return (
int(event.data["actor_id"]) >= 0
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_WOODPILE
and item_id == SimulationIds.RESOURCE_WOOD
and (
StringName(event.data.get("action_id", &""))
in [SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY]
)
and float(event.data.get("required_amount", 0.0)) > 0.0
)
if float(event.data["amount"]) <= 0.0:
return false
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
return item_id == SimulationIds.RESOURCE_FOOD
if event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED:
var player_actor_id := int(event.data["actor_id"])
if player_actor_id != SimulationIds.PLAYER_ACTOR_ID:
return false
return (
StringName(event.data["destination_id"])
in [
SimulationIds.STORAGE_VILLAGE_PANTRY,
SimulationIds.STORAGE_VILLAGE_WOODPILE,
]
)
if event_type != SimulationIds.EVENT_STORAGE_WITHDRAWN:
return false
var actor_id := int(event.data["actor_id"])
return (
actor_id >= 0
and item_id == SimulationIds.RESOURCE_FOOD
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
and (StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id))
)
static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC:
for npc in npcs:
if npc.id == npc_id:
return npc
return null
static func _key(knower_id: int, event_id: int) -> String:
return "%d:%d" % [knower_id, event_id]
static func _sort_records(first: KnownEventStateRecord, second: KnownEventStateRecord) -> bool:
if first.get_knower_id() != second.get_knower_id():
return first.get_knower_id() < second.get_knower_id()
return first.get_event_id() < second.get_event_id()
static func _sort_by_acquisition(
first: KnownEventStateRecord, second: KnownEventStateRecord
) -> bool:
if first.get_acquired_tick() != second.get_acquired_tick():
return first.get_acquired_tick() < second.get_acquired_tick()
return first.get_event_id() < second.get_event_id()
static func _sort_by_importance(first: Dictionary, second: Dictionary) -> bool:
if bool(first["lasting"]) != bool(second["lasting"]):
return bool(first["lasting"])
if 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"])