feat: add witnessed knowledge and wind ambience
This commit is contained in:
@@ -32,7 +32,6 @@ var has_travel_target := false
|
||||
var inventory: Dictionary = {}
|
||||
var last_task: StringName
|
||||
var random_source: RandomNumberGenerator
|
||||
var familiarity: Dictionary = {}
|
||||
var mourning_ticks := 0
|
||||
var debug_logs := false
|
||||
|
||||
|
||||
+139
-53
@@ -2,6 +2,8 @@ extends Node
|
||||
|
||||
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
||||
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
||||
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
||||
|
||||
signal npc_task_changed(npc: SimNPC, old_task: StringName, new_task: StringName)
|
||||
signal village_changed(village: SimVillage)
|
||||
@@ -12,6 +14,8 @@ signal npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float)
|
||||
signal economic_event_recorded(event: EconomicEventRecord)
|
||||
signal state_restored
|
||||
signal npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult)
|
||||
signal relationship_changed(relationship: RelationshipStateRecord, cause_event: EconomicEventRecord)
|
||||
signal event_knowledge_changed(knower_id: int, event: EconomicEventRecord)
|
||||
|
||||
var village := SimVillage.new()
|
||||
|
||||
@@ -29,6 +33,8 @@ var wander_random_sources := {}
|
||||
var resource_states: Dictionary = {}
|
||||
var event_log := SimulationEventLogScript.new()
|
||||
var economy := VillageEconomyScript.new()
|
||||
var relationship_system := RelationshipSystemScript.new()
|
||||
var event_knowledge_system := EventKnowledgeSystemScript.new()
|
||||
var storage_states: Dictionary:
|
||||
get:
|
||||
return economy.storage_states
|
||||
@@ -57,6 +63,7 @@ func _ready() -> void:
|
||||
economy.inventory_changed.connect(_on_economy_inventory_changed)
|
||||
economy.economic_event_requested.connect(_record_economic_event)
|
||||
economy.narrative_event_requested.connect(record_narrative_event)
|
||||
action_selector.relationship_system = relationship_system
|
||||
var definition_errors := SimulationDefinitions.validate()
|
||||
if not definition_errors.is_empty():
|
||||
for error in definition_errors:
|
||||
@@ -72,6 +79,7 @@ func _ready() -> void:
|
||||
village.update_modifiers()
|
||||
village.update_priorities()
|
||||
generate_npcs()
|
||||
relationship_system.initialize_households(npcs)
|
||||
call_deferred("register_loaded_resource_nodes")
|
||||
call_deferred("register_loaded_storage_nodes")
|
||||
if debug_logs:
|
||||
@@ -122,12 +130,6 @@ func generate_npcs() -> void:
|
||||
for i in range(npcs.size()):
|
||||
npcs[i].home_position = home_positions[i % home_count]
|
||||
|
||||
for i in range(npcs.size()):
|
||||
for j in range(i + 1, npcs.size()):
|
||||
if npcs[i].home_position.distance_to(npcs[j].home_position) < 4.0:
|
||||
npcs[i].familiarity[npcs[j].id] = 0.5
|
||||
npcs[j].familiarity[npcs[i].id] = 0.5
|
||||
|
||||
|
||||
func _create_random_source(npc_id: int, stream_id: int) -> RandomNumberGenerator:
|
||||
var source := RandomNumberGenerator.new()
|
||||
@@ -200,13 +202,13 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
|
||||
npc_decision_recorded.emit(npc, selection)
|
||||
npc.set_task(selection.action_id, selection.duration_override)
|
||||
var definition := SimulationDefinitions.get_action(selection.action_id)
|
||||
var display_name := definition.display_name if definition != null else String(selection.action_id)
|
||||
var display_name := (
|
||||
definition.display_name if definition != null else String(selection.action_id)
|
||||
)
|
||||
record_narrative_event(SimulationIds.EVENT_TASK_STARTED, npc.id, &"", display_name)
|
||||
|
||||
|
||||
func _handle_npc_death(
|
||||
npc: SimNPC, previous_task: StringName, previous_target: StringName
|
||||
) -> void:
|
||||
func _handle_npc_death(npc: SimNPC, previous_task: StringName, previous_target: StringName) -> void:
|
||||
if not previous_target.is_empty():
|
||||
release_npc_reservation(npc.id)
|
||||
npc_died.emit(npc)
|
||||
@@ -264,8 +266,10 @@ func _complete_resource_gather(npc: SimNPC, completed_task: StringName) -> void:
|
||||
if resource_state == null or resource_state.get_reserved_by() != npc.id:
|
||||
if debug_logs:
|
||||
print(
|
||||
"[SimulationManager] %s could not complete %s: invalid reservation"
|
||||
% [npc.npc_name, completed_task]
|
||||
(
|
||||
"[SimulationManager] %s could not complete %s: invalid reservation"
|
||||
% [npc.npc_name, completed_task]
|
||||
)
|
||||
)
|
||||
release_npc_reservation(npc.id)
|
||||
return
|
||||
@@ -289,8 +293,10 @@ func _complete_resource_gather(npc: SimNPC, completed_task: StringName) -> void:
|
||||
)
|
||||
if debug_logs:
|
||||
print(
|
||||
"[SimulationManager] %s extracted %.1f %s from %s"
|
||||
% [npc.npc_name, extracted, resource_id, resource_state.get_node_id()]
|
||||
(
|
||||
"[SimulationManager] %s extracted %.1f %s from %s"
|
||||
% [npc.npc_name, extracted, resource_id, resource_state.get_node_id()]
|
||||
)
|
||||
)
|
||||
release_npc_reservation(npc.id)
|
||||
|
||||
@@ -384,16 +390,12 @@ func notify_npc_arrived(npc_id: int) -> void:
|
||||
continue
|
||||
if (
|
||||
other.target_id == npc.target_id
|
||||
and other.task_state in [
|
||||
SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING
|
||||
]
|
||||
and (
|
||||
other.task_state
|
||||
in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
|
||||
)
|
||||
):
|
||||
npc.familiarity[other.id] = minf(
|
||||
float(npc.familiarity.get(other.id, 0.0)) + 0.1, 1.0
|
||||
)
|
||||
other.familiarity[npc.id] = minf(
|
||||
float(other.familiarity.get(npc.id, 0.0)) + 0.1, 1.0
|
||||
)
|
||||
relationship_system.increase_shared_work_familiarity(npc.id, other.id)
|
||||
|
||||
if debug_logs:
|
||||
print(
|
||||
@@ -491,27 +493,12 @@ func get_activity_target_claim_count(target_id: StringName, except_npc_id: int =
|
||||
|
||||
|
||||
func _notify_mourning(dead_npc: SimNPC) -> void:
|
||||
var best_id := -1
|
||||
var best_score := -1.0
|
||||
for key in dead_npc.familiarity:
|
||||
var other_id: int = int(key)
|
||||
var score: float = float(dead_npc.familiarity[key])
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_id = other_id
|
||||
if best_id < 0:
|
||||
var mourner := relationship_system.get_most_familiar_living_subject(dead_npc.id, npcs)
|
||||
if mourner == null:
|
||||
return
|
||||
for npc in npcs:
|
||||
if npc.id == best_id and not npc.is_dead:
|
||||
npc.mourning_ticks = 100
|
||||
if debug_logs:
|
||||
print(
|
||||
"[SimulationManager] ",
|
||||
npc.npc_name,
|
||||
" is mourning ",
|
||||
dead_npc.npc_name
|
||||
)
|
||||
return
|
||||
mourner.mourning_ticks = 100
|
||||
if debug_logs:
|
||||
print("[SimulationManager] ", mourner.npc_name, " is mourning ", dead_npc.npc_name)
|
||||
|
||||
|
||||
func _record_economic_event(
|
||||
@@ -521,19 +508,66 @@ func _record_economic_event(
|
||||
destination_id: StringName,
|
||||
item_id: StringName,
|
||||
amount: float
|
||||
) -> void:
|
||||
_record_economic_event_at(
|
||||
event_type,
|
||||
actor_id,
|
||||
source_id,
|
||||
destination_id,
|
||||
item_id,
|
||||
amount,
|
||||
_get_event_world_position(actor_id, source_id, destination_id)
|
||||
)
|
||||
|
||||
|
||||
func _record_economic_event_at(
|
||||
event_type: StringName,
|
||||
actor_id: int,
|
||||
source_id: StringName,
|
||||
destination_id: StringName,
|
||||
item_id: StringName,
|
||||
amount: float,
|
||||
world_position: Vector3
|
||||
) -> void:
|
||||
event_log.record_economic(
|
||||
tick_count, event_type, actor_id, source_id, destination_id, item_id, amount
|
||||
tick_count, event_type, actor_id, source_id, destination_id, item_id, amount, world_position
|
||||
)
|
||||
|
||||
|
||||
func record_narrative_event(
|
||||
event_type: StringName, actor_id: int, source_id: StringName = &"", action_display: String = ""
|
||||
) -> void:
|
||||
_record_narrative_event_at(
|
||||
event_type,
|
||||
actor_id,
|
||||
source_id,
|
||||
action_display,
|
||||
_get_event_world_position(actor_id, source_id, &"")
|
||||
)
|
||||
|
||||
|
||||
func _record_narrative_event_at(
|
||||
event_type: StringName,
|
||||
actor_id: int,
|
||||
source_id: StringName = &"",
|
||||
action_display: String = ""
|
||||
source_id: StringName,
|
||||
action_display: String,
|
||||
world_position: Vector3
|
||||
) -> void:
|
||||
event_log.record_narrative(tick_count, event_type, actor_id, source_id, action_display)
|
||||
event_log.record_narrative(
|
||||
tick_count, event_type, actor_id, source_id, action_display, world_position
|
||||
)
|
||||
|
||||
|
||||
func _get_event_world_position(
|
||||
actor_id: int, source_id: StringName, destination_id: StringName
|
||||
) -> Vector3:
|
||||
for npc in npcs:
|
||||
if npc.id != actor_id:
|
||||
continue
|
||||
if not npc.target_id.is_empty() and npc.target_id in [source_id, destination_id]:
|
||||
return npc.travel_target_position
|
||||
return npc.position
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
func get_npc_events(npc_id: int, max_count: int = 8) -> Array[EconomicEventRecord]:
|
||||
@@ -545,9 +579,43 @@ func get_recent_events(max_count: int = 5) -> Array[EconomicEventRecord]:
|
||||
|
||||
|
||||
func _on_economic_event_recorded(event: EconomicEventRecord) -> void:
|
||||
var learned_records: Array[KnownEventStateRecord] = event_knowledge_system.observe_event(
|
||||
event, npcs
|
||||
)
|
||||
for learned_record in learned_records:
|
||||
event_knowledge_changed.emit(learned_record.get_knower_id(), event)
|
||||
var knower_ids: Array[int] = event_knowledge_system.get_knowers(int(event.data["event_id"]))
|
||||
var changed_relationships: Array[RelationshipStateRecord] = relationship_system.apply_event(
|
||||
event, npcs, knower_ids
|
||||
)
|
||||
for relationship in changed_relationships:
|
||||
relationship_changed.emit(relationship, event)
|
||||
economic_event_recorded.emit(event)
|
||||
|
||||
|
||||
func get_primary_relationship(npc_id: int) -> RelationshipStateRecord:
|
||||
return relationship_system.get_primary_relationship(npc_id)
|
||||
|
||||
|
||||
func get_relationship_cause(relationship: RelationshipStateRecord) -> EconomicEventRecord:
|
||||
if relationship == null:
|
||||
return null
|
||||
return event_log.get_by_id(relationship.get_last_trust_cause_event_id())
|
||||
|
||||
|
||||
func get_known_events(npc_id: int, max_count: int = 3) -> Array[EconomicEventRecord]:
|
||||
var known: Array[EconomicEventRecord] = []
|
||||
for event_id in event_knowledge_system.get_known_event_ids(npc_id, max_count):
|
||||
var event := event_log.get_by_id(event_id)
|
||||
if event != null:
|
||||
known.append(event)
|
||||
return known
|
||||
|
||||
|
||||
func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
||||
return event_knowledge_system.knows_event(npc_id, event_id)
|
||||
|
||||
|
||||
func get_current_speed() -> String:
|
||||
return "%.2fx" % SPEED_LEVELS[speed_index]
|
||||
|
||||
@@ -576,8 +644,10 @@ func register_storage_node(node: StorageNode) -> bool:
|
||||
var storage_state := storage_states.get(node.storage_id) as StorageStateRecord
|
||||
if storage_state == null:
|
||||
push_error(
|
||||
"SimulationManager: StorageNode '%s' has no authoritative StorageStateRecord"
|
||||
% node.storage_id
|
||||
(
|
||||
"SimulationManager: StorageNode '%s' has no authoritative StorageStateRecord"
|
||||
% node.storage_id
|
||||
)
|
||||
)
|
||||
return false
|
||||
return node.bind_state(storage_state)
|
||||
@@ -671,7 +741,12 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
||||
return 0.0
|
||||
|
||||
var deposited := economy.deposit_resource(resource_state.get_resource_id(), extracted)
|
||||
_record_economic_event(
|
||||
var event_position := (
|
||||
node.interaction_point.global_position
|
||||
if node.interaction_point != null
|
||||
else node.global_position
|
||||
)
|
||||
_record_economic_event_at(
|
||||
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
||||
-1,
|
||||
resource_state.get_node_id(),
|
||||
@@ -685,11 +760,16 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
||||
)
|
||||
),
|
||||
resource_state.get_resource_id(),
|
||||
deposited
|
||||
deposited,
|
||||
event_position
|
||||
)
|
||||
if resource_state.get_amount_remaining() <= 0.0:
|
||||
record_narrative_event(
|
||||
SimulationIds.EVENT_RESOURCE_DEPLETED, -1, resource_state.get_node_id()
|
||||
_record_narrative_event_at(
|
||||
SimulationIds.EVENT_RESOURCE_DEPLETED,
|
||||
-1,
|
||||
resource_state.get_node_id(),
|
||||
"",
|
||||
event_position
|
||||
)
|
||||
village_changed.emit(village)
|
||||
|
||||
@@ -818,6 +898,10 @@ func create_state_record() -> SimulationStateRecord:
|
||||
record.storages.append(storage_state)
|
||||
for event in economic_events:
|
||||
record.economic_events.append(event)
|
||||
for relationship in relationship_system.get_all_sorted():
|
||||
record.relationships.append(relationship)
|
||||
for known_event in event_knowledge_system.get_all_sorted():
|
||||
record.event_knowledge.append(known_event)
|
||||
return record
|
||||
|
||||
|
||||
@@ -853,6 +937,8 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
npcs.clear()
|
||||
for npc_record in record.npcs:
|
||||
npcs.append(npc_record.restore(debug_logs))
|
||||
relationship_system.restore(record.relationships)
|
||||
event_knowledge_system.restore(record.event_knowledge)
|
||||
|
||||
wander_random_sources.clear()
|
||||
var wander_streams: Array = record.simulation["wander_random_streams"]
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
class_name ActionSelectionSystem
|
||||
extends RefCounted
|
||||
|
||||
enum SchedulePeriod {
|
||||
WORK,
|
||||
SLEEP,
|
||||
MEAL,
|
||||
DISCRETIONARY
|
||||
}
|
||||
enum SchedulePeriod { WORK, SLEEP, MEAL, DISCRETIONARY }
|
||||
|
||||
const SLEEP_BEGIN := 0.88
|
||||
const SLEEP_END := 0.15
|
||||
@@ -17,9 +12,14 @@ const DINNER_END := 0.8
|
||||
const WORK_BEGIN := 0.28
|
||||
const WORK_END := 0.85
|
||||
const UNAVAILABLE_ACTION_SCORE := -1000000.0
|
||||
const RELATIONSHIP_AID_PANTRY_THRESHOLD := 25.0
|
||||
|
||||
var relationship_system: RefCounted
|
||||
|
||||
|
||||
func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, all_npcs: Array = []) -> ActionSelectionResult:
|
||||
func select_action(
|
||||
npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, all_npcs: Array = []
|
||||
) -> ActionSelectionResult:
|
||||
if npc.is_dead:
|
||||
return null
|
||||
|
||||
@@ -70,9 +70,7 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, a
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_EAT, -1.0, "Night-time hunger; eating from inventory"
|
||||
)
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_SLEEP, -1.0, "Night-time; going home"
|
||||
)
|
||||
return ActionSelectionResult.new(SimulationIds.ACTION_SLEEP, -1.0, "Night-time; going home")
|
||||
|
||||
if period == SchedulePeriod.MEAL:
|
||||
if npc.hunger > 50.0:
|
||||
@@ -112,6 +110,16 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, a
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold"
|
||||
)
|
||||
if village.food <= RELATIONSHIP_AID_PANTRY_THRESHOLD and relationship_system != null:
|
||||
var trusted_starving_npc: SimNPC = relationship_system.get_trusted_starving_subject(
|
||||
npc.id, all_npcs
|
||||
)
|
||||
if trusted_starving_npc != null:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_GATHER_FOOD,
|
||||
-1.0,
|
||||
"Helping %s, a trusted acquaintance who is starving" % trusted_starving_npc.npc_name
|
||||
)
|
||||
|
||||
if period == SchedulePeriod.DISCRETIONARY:
|
||||
var roll := npc.random_source.randf()
|
||||
@@ -120,10 +128,10 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, a
|
||||
SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander"
|
||||
)
|
||||
|
||||
return _choose_best_work_action(npc, village, all_npcs)
|
||||
return _choose_best_work_action(npc, village)
|
||||
|
||||
|
||||
func _choose_best_work_action(npc: SimNPC, village: SimVillage, all_npcs: Array) -> ActionSelectionResult:
|
||||
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
||||
var scores := {
|
||||
SimulationIds.ACTION_GATHER_FOOD:
|
||||
_calculate_score(
|
||||
@@ -177,16 +185,6 @@ func _choose_best_work_action(npc: SimNPC, village: SimVillage, all_npcs: Array)
|
||||
if not rejection.is_empty():
|
||||
scores[action_id] = UNAVAILABLE_ACTION_SCORE
|
||||
rejections[action_id] = rejection
|
||||
for familiar_id in npc.familiarity:
|
||||
if not familiar_id is int:
|
||||
continue
|
||||
var other_task: StringName
|
||||
for other in all_npcs:
|
||||
if other.id == familiar_id and not other.is_dead:
|
||||
other_task = other.current_task
|
||||
break
|
||||
if other_task in scores:
|
||||
scores[other_task] = float(scores[other_task]) + 0.3
|
||||
var best_action := SimulationIds.ACTION_GATHER_FOOD
|
||||
var best_score: float = scores[best_action]
|
||||
for action_id in [
|
||||
@@ -207,11 +205,14 @@ func _get_cost_rejection(definition: ActionDefinition, village: SimVillage) -> S
|
||||
var available := _get_village_resource_amount(village, definition.completion_cost_resource_id)
|
||||
if available >= definition.completion_cost_amount:
|
||||
return ""
|
||||
return "Needs %.0f %s (%.1f available)" % [
|
||||
definition.completion_cost_amount,
|
||||
String(definition.completion_cost_resource_id).capitalize(),
|
||||
available,
|
||||
]
|
||||
return (
|
||||
"Needs %.0f %s (%.1f available)"
|
||||
% [
|
||||
definition.completion_cost_amount,
|
||||
String(definition.completion_cost_resource_id).capitalize(),
|
||||
available,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
func _get_village_resource_amount(village: SimVillage, resource_id: StringName) -> float:
|
||||
|
||||
@@ -57,7 +57,9 @@ func _resolve_activity(
|
||||
|
||||
var best: Dictionary = {}
|
||||
var best_distance := INF
|
||||
var candidates: Array[Dictionary] = active_world_adapter.get_activity_candidates(npc.current_task)
|
||||
var candidates: Array[Dictionary] = active_world_adapter.get_activity_candidates(
|
||||
npc.current_task
|
||||
)
|
||||
for candidate in candidates:
|
||||
var target_id := StringName(candidate["target_id"])
|
||||
var capacity := maxi(int(candidate.get("capacity", 1)), 1)
|
||||
|
||||
@@ -131,8 +131,10 @@ static func validate() -> Array[String]:
|
||||
)
|
||||
if (
|
||||
not definition.completion_cost_resource_id.is_empty()
|
||||
and definition.completion_cost_resource_id
|
||||
not in [SimulationIds.RESOURCE_FOOD, SimulationIds.RESOURCE_WOOD]
|
||||
and (
|
||||
definition.completion_cost_resource_id
|
||||
not in [SimulationIds.RESOURCE_FOOD, SimulationIds.RESOURCE_WOOD]
|
||||
)
|
||||
):
|
||||
errors.append(
|
||||
(
|
||||
|
||||
@@ -144,9 +144,7 @@ func consume_npc_food(npc: SimNPC) -> bool:
|
||||
npc.starvation_ticks = 0
|
||||
npc.is_starving = npc.hunger >= 90.0
|
||||
inventory_changed.emit(
|
||||
npc,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
||||
npc, SimulationIds.RESOURCE_FOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
||||
)
|
||||
economic_event_requested.emit(
|
||||
SimulationIds.EVENT_ITEM_CONSUMED,
|
||||
@@ -167,12 +165,15 @@ func consume_completion_cost(npc: SimNPC, definition: ActionDefinition) -> bool:
|
||||
var storage := get_storage_for_resource(resource_id)
|
||||
var available := storage.get_amount(resource_id) if storage != null else 0.0
|
||||
if storage == null or available < required_amount:
|
||||
var reason := "%s: needs %.0f %s (%.1f available)" % [
|
||||
definition.display_name,
|
||||
required_amount,
|
||||
String(resource_id).capitalize(),
|
||||
available,
|
||||
]
|
||||
var reason := (
|
||||
"%s: needs %.0f %s (%.1f available)"
|
||||
% [
|
||||
definition.display_name,
|
||||
required_amount,
|
||||
String(resource_id).capitalize(),
|
||||
available,
|
||||
]
|
||||
)
|
||||
narrative_event_requested.emit(
|
||||
SimulationIds.EVENT_TASK_BLOCKED,
|
||||
npc.id,
|
||||
@@ -194,8 +195,10 @@ func consume_completion_cost(npc: SimNPC, definition: ActionDefinition) -> bool:
|
||||
)
|
||||
if debug_logs:
|
||||
print(
|
||||
"[VillageEconomy] %s consumed %.1f %s for %s"
|
||||
% [npc.npc_name, consumed, resource_id, definition.action_id]
|
||||
(
|
||||
"[VillageEconomy] %s consumed %.1f %s for %s"
|
||||
% [npc.npc_name, consumed, resource_id, definition.action_id]
|
||||
)
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
@@ -16,12 +16,21 @@ func record_economic(
|
||||
source_id: StringName,
|
||||
destination_id: StringName,
|
||||
item_id: StringName,
|
||||
amount: float
|
||||
amount: float,
|
||||
world_position: Vector3 = Vector3.ZERO
|
||||
) -> EconomicEventRecord:
|
||||
if amount <= 0.0:
|
||||
return null
|
||||
var event := EconomicEventRecord.create(
|
||||
next_event_id, event_type, tick, actor_id, source_id, destination_id, item_id, amount
|
||||
next_event_id,
|
||||
event_type,
|
||||
tick,
|
||||
actor_id,
|
||||
source_id,
|
||||
destination_id,
|
||||
item_id,
|
||||
amount,
|
||||
world_position
|
||||
)
|
||||
_append(event)
|
||||
return event
|
||||
@@ -32,10 +41,11 @@ func record_narrative(
|
||||
event_type: StringName,
|
||||
actor_id: int,
|
||||
source_id: StringName = &"",
|
||||
action_display: String = ""
|
||||
action_display: String = "",
|
||||
world_position: Vector3 = Vector3.ZERO
|
||||
) -> EconomicEventRecord:
|
||||
var event := EconomicEventRecord.create_narrative(
|
||||
next_event_id, event_type, tick, actor_id, source_id, action_display
|
||||
next_event_id, event_type, tick, actor_id, source_id, action_display, world_position
|
||||
)
|
||||
_append(event)
|
||||
return event
|
||||
@@ -62,6 +72,13 @@ func get_recent(max_count: int = 5) -> Array[EconomicEventRecord]:
|
||||
return results
|
||||
|
||||
|
||||
func get_by_id(event_id: int) -> EconomicEventRecord:
|
||||
for event in events:
|
||||
if int(event.data["event_id"]) == event_id:
|
||||
return event
|
||||
return null
|
||||
|
||||
|
||||
func get_consumption_rates(
|
||||
current_tick: int,
|
||||
window_ticks: int = DEFAULT_RATE_WINDOW_TICKS,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
class_name EventKnowledgeSystem
|
||||
extends RefCounted
|
||||
|
||||
const WITNESS_RADIUS := 10.0
|
||||
|
||||
var known_events: Dictionary = {}
|
||||
|
||||
|
||||
func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[KnownEventStateRecord]:
|
||||
var learned: Array[KnownEventStateRecord] = []
|
||||
if not _is_witnessable(event):
|
||||
return learned
|
||||
var actor := _find_npc(int(event.data["actor_id"]), npcs)
|
||||
if actor == null:
|
||||
return learned
|
||||
var actor_record := _remember(actor.id, int(event.data["event_id"]))
|
||||
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.id == actor.id or npc.is_dead:
|
||||
continue
|
||||
if npc.position.distance_squared_to(event_position) > witness_radius_squared:
|
||||
continue
|
||||
var witness_record := _remember(npc.id, int(event.data["event_id"]))
|
||||
if witness_record != null:
|
||||
learned.append(witness_record)
|
||||
learned.sort_custom(_sort_records)
|
||||
return learned
|
||||
|
||||
|
||||
func knows_event(knower_id: int, event_id: int) -> bool:
|
||||
return known_events.has(_key(knower_id, event_id))
|
||||
|
||||
|
||||
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 event_ids: Array[int] = []
|
||||
for record in known_events.values():
|
||||
if record.get_knower_id() == knower_id:
|
||||
event_ids.append(record.get_event_id())
|
||||
event_ids.sort()
|
||||
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_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) -> KnownEventStateRecord:
|
||||
var key := _key(knower_id, event_id)
|
||||
if known_events.has(key):
|
||||
return null
|
||||
var record := KnownEventStateRecord.create(knower_id, event_id)
|
||||
known_events[key] = record
|
||||
return record
|
||||
|
||||
|
||||
func _is_witnessable(event: EconomicEventRecord) -> bool:
|
||||
return (
|
||||
StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||
and float(event.data["amount"]) > 0.0
|
||||
)
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://8qgicjoe2gft
|
||||
@@ -0,0 +1,173 @@
|
||||
extends RefCounted
|
||||
|
||||
const AID_HUNGER_THRESHOLD := 80.0
|
||||
const TRUSTED_HELP_THRESHOLD := 0.6
|
||||
const FOOD_AID_TRUST_GAIN := 0.15
|
||||
const SHARED_WORK_FAMILIARITY_GAIN := 0.1
|
||||
|
||||
var relationships: Dictionary = {}
|
||||
|
||||
|
||||
func initialize_households(npcs: Array[SimNPC]) -> void:
|
||||
relationships.clear()
|
||||
for first_index in range(npcs.size()):
|
||||
for second_index in range(first_index + 1, npcs.size()):
|
||||
var first := npcs[first_index]
|
||||
var second := npcs[second_index]
|
||||
if first.home_position.distance_to(second.home_position) >= 4.0:
|
||||
continue
|
||||
_add(RelationshipStateRecord.create(first.id, second.id, 0.5))
|
||||
_add(RelationshipStateRecord.create(second.id, first.id, 0.5))
|
||||
|
||||
|
||||
func restore(records: Array[RelationshipStateRecord]) -> void:
|
||||
relationships.clear()
|
||||
for record in records:
|
||||
_add(record)
|
||||
|
||||
|
||||
func get_all_sorted() -> Array[RelationshipStateRecord]:
|
||||
var records: Array[RelationshipStateRecord] = []
|
||||
for record in relationships.values():
|
||||
records.append(record)
|
||||
records.sort_custom(_sort_relationships)
|
||||
return records
|
||||
|
||||
|
||||
func get_relationship(observer_id: int, subject_id: int) -> RelationshipStateRecord:
|
||||
return relationships.get(_key(observer_id, subject_id)) as RelationshipStateRecord
|
||||
|
||||
|
||||
func increase_shared_work_familiarity(first_id: int, second_id: int) -> void:
|
||||
var first_to_second := _get_or_create(first_id, second_id)
|
||||
var second_to_first := _get_or_create(second_id, first_id)
|
||||
first_to_second.increase_familiarity(SHARED_WORK_FAMILIARITY_GAIN)
|
||||
second_to_first.increase_familiarity(SHARED_WORK_FAMILIARITY_GAIN)
|
||||
|
||||
|
||||
func apply_event(
|
||||
event: EconomicEventRecord, npcs: Array[SimNPC], knower_ids: Array[int]
|
||||
) -> Array[RelationshipStateRecord]:
|
||||
var changed: Array[RelationshipStateRecord] = []
|
||||
if StringName(event.data["event_type"]) != SimulationIds.EVENT_STORAGE_DEPOSITED:
|
||||
return changed
|
||||
if StringName(event.data["item_id"]) != SimulationIds.RESOURCE_FOOD:
|
||||
return changed
|
||||
var contributor_id := int(event.data["actor_id"])
|
||||
var event_id := int(event.data["event_id"])
|
||||
if contributor_id < 0 or event_id < 0 or float(event.data["amount"]) <= 0.0:
|
||||
return changed
|
||||
for observer in npcs:
|
||||
if observer.id == contributor_id or observer.is_dead:
|
||||
continue
|
||||
if observer.id not in knower_ids:
|
||||
continue
|
||||
if observer.hunger < AID_HUNGER_THRESHOLD:
|
||||
continue
|
||||
var relationship := get_relationship(observer.id, contributor_id)
|
||||
if relationship == null or relationship.get_familiarity() <= 0.0:
|
||||
continue
|
||||
if event_id <= relationship.get_last_trust_cause_event_id():
|
||||
continue
|
||||
var applied := relationship.increase_trust(FOOD_AID_TRUST_GAIN, event_id)
|
||||
if applied <= 0.0:
|
||||
continue
|
||||
changed.append(relationship)
|
||||
return changed
|
||||
|
||||
|
||||
func get_primary_relationship(observer_id: int) -> RelationshipStateRecord:
|
||||
var best: RelationshipStateRecord
|
||||
for relationship in relationships.values():
|
||||
if relationship.get_observer_id() != observer_id:
|
||||
continue
|
||||
if best == null or _is_stronger(relationship, best):
|
||||
best = relationship
|
||||
return best
|
||||
|
||||
|
||||
func get_most_familiar_living_subject(observer_id: int, npcs: Array[SimNPC]) -> SimNPC:
|
||||
var living_by_id := {}
|
||||
for npc in npcs:
|
||||
if not npc.is_dead:
|
||||
living_by_id[npc.id] = npc
|
||||
var best_relationship: RelationshipStateRecord
|
||||
for relationship in relationships.values():
|
||||
if relationship.get_observer_id() != observer_id:
|
||||
continue
|
||||
if not living_by_id.has(relationship.get_subject_id()):
|
||||
continue
|
||||
if (
|
||||
best_relationship == null
|
||||
or relationship.get_familiarity() > best_relationship.get_familiarity()
|
||||
or (
|
||||
is_equal_approx(relationship.get_familiarity(), best_relationship.get_familiarity())
|
||||
and relationship.get_subject_id() < best_relationship.get_subject_id()
|
||||
)
|
||||
):
|
||||
best_relationship = relationship
|
||||
if best_relationship == null:
|
||||
return null
|
||||
return living_by_id[best_relationship.get_subject_id()] as SimNPC
|
||||
|
||||
|
||||
func get_trusted_starving_subject(observer_id: int, npcs: Array[SimNPC]) -> SimNPC:
|
||||
var starving_by_id := {}
|
||||
for npc in npcs:
|
||||
if not npc.is_dead and npc.is_starving:
|
||||
starving_by_id[npc.id] = npc
|
||||
var best_relationship: RelationshipStateRecord
|
||||
for relationship in relationships.values():
|
||||
if relationship.get_observer_id() != observer_id:
|
||||
continue
|
||||
if relationship.get_trust() < TRUSTED_HELP_THRESHOLD:
|
||||
continue
|
||||
if not starving_by_id.has(relationship.get_subject_id()):
|
||||
continue
|
||||
if (
|
||||
best_relationship == null
|
||||
or relationship.get_trust() > best_relationship.get_trust()
|
||||
or (
|
||||
is_equal_approx(relationship.get_trust(), best_relationship.get_trust())
|
||||
and relationship.get_subject_id() < best_relationship.get_subject_id()
|
||||
)
|
||||
):
|
||||
best_relationship = relationship
|
||||
if best_relationship == null:
|
||||
return null
|
||||
return starving_by_id[best_relationship.get_subject_id()] as SimNPC
|
||||
|
||||
|
||||
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:
|
||||
relationships[_key(relationship.get_observer_id(), relationship.get_subject_id())] = relationship
|
||||
|
||||
|
||||
static func _key(observer_id: int, subject_id: int) -> String:
|
||||
return "%d:%d" % [observer_id, subject_id]
|
||||
|
||||
|
||||
static func _sort_relationships(
|
||||
first: RelationshipStateRecord, second: RelationshipStateRecord
|
||||
) -> bool:
|
||||
if first.get_observer_id() != second.get_observer_id():
|
||||
return first.get_observer_id() < second.get_observer_id()
|
||||
return first.get_subject_id() < second.get_subject_id()
|
||||
|
||||
|
||||
static func _is_stronger(
|
||||
candidate: RelationshipStateRecord, current: RelationshipStateRecord
|
||||
) -> bool:
|
||||
if not is_equal_approx(candidate.get_trust(), current.get_trust()):
|
||||
return candidate.get_trust() > current.get_trust()
|
||||
if not is_equal_approx(candidate.get_familiarity(), current.get_familiarity()):
|
||||
return candidate.get_familiarity() > current.get_familiarity()
|
||||
return candidate.get_subject_id() < current.get_subject_id()
|
||||
@@ -0,0 +1 @@
|
||||
uid://d4hehra0mfvdf
|
||||
@@ -1,7 +1,8 @@
|
||||
class_name EconomicEventRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const SCHEMA_VERSION := 2
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
|
||||
var data: Dictionary
|
||||
|
||||
@@ -18,7 +19,8 @@ static func create(
|
||||
source_id: StringName,
|
||||
destination_id: StringName,
|
||||
item_id: StringName,
|
||||
amount: float
|
||||
amount: float,
|
||||
world_position: Vector3 = Vector3.ZERO
|
||||
) -> EconomicEventRecord:
|
||||
return EconomicEventRecord.new(
|
||||
{
|
||||
@@ -30,7 +32,8 @@ static func create(
|
||||
"source_id": String(source_id),
|
||||
"destination_id": String(destination_id),
|
||||
"item_id": String(item_id),
|
||||
"amount": amount
|
||||
"amount": amount,
|
||||
"world_position": [world_position.x, world_position.y, world_position.z]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -41,7 +44,8 @@ static func create_narrative(
|
||||
tick: int,
|
||||
actor_id: int,
|
||||
source_id: StringName,
|
||||
action_display: String = ""
|
||||
action_display: String = "",
|
||||
world_position: Vector3 = Vector3.ZERO
|
||||
) -> EconomicEventRecord:
|
||||
return EconomicEventRecord.new(
|
||||
{
|
||||
@@ -54,13 +58,15 @@ static func create_narrative(
|
||||
"destination_id": "",
|
||||
"item_id": "",
|
||||
"amount": 0.0,
|
||||
"action_name": action_display
|
||||
"action_name": action_display,
|
||||
"world_position": [world_position.x, world_position.y, world_position.z]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
var version := int(record_data.get("schema_version", -1))
|
||||
if version not in [LEGACY_SCHEMA_VERSION, SCHEMA_VERSION]:
|
||||
return null
|
||||
if not record_data.has_all(
|
||||
[
|
||||
@@ -75,6 +81,8 @@ static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord:
|
||||
]
|
||||
):
|
||||
return null
|
||||
if version == SCHEMA_VERSION and not record_data.has("world_position"):
|
||||
return null
|
||||
var normalized := record_data.duplicate(true)
|
||||
normalized["schema_version"] = SCHEMA_VERSION
|
||||
normalized["event_id"] = int(record_data["event_id"])
|
||||
@@ -85,11 +93,13 @@ static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord:
|
||||
normalized["destination_id"] = String(record_data["destination_id"])
|
||||
normalized["item_id"] = String(record_data["item_id"])
|
||||
normalized["amount"] = float(record_data["amount"])
|
||||
if (
|
||||
normalized["event_id"] < 0
|
||||
or normalized["tick"] < 0
|
||||
or normalized["event_type"].is_empty()
|
||||
):
|
||||
var saved_position = record_data.get("world_position", [0.0, 0.0, 0.0])
|
||||
if not saved_position is Array or saved_position.size() != 3:
|
||||
return null
|
||||
normalized["world_position"] = [
|
||||
float(saved_position[0]), float(saved_position[1]), float(saved_position[2])
|
||||
]
|
||||
if normalized["event_id"] < 0 or normalized["tick"] < 0 or normalized["event_type"].is_empty():
|
||||
return null
|
||||
return EconomicEventRecord.new(normalized)
|
||||
|
||||
@@ -98,6 +108,11 @@ func to_dictionary() -> Dictionary:
|
||||
return data.duplicate(true)
|
||||
|
||||
|
||||
func get_world_position() -> Vector3:
|
||||
var saved_position: Array = data["world_position"]
|
||||
return Vector3(float(saved_position[0]), float(saved_position[1]), float(saved_position[2]))
|
||||
|
||||
|
||||
func description(npc_names: Dictionary = {}) -> String:
|
||||
var actor_name: String = npc_names.get(int(data["actor_id"]), "Someone")
|
||||
var item: String = str(data["item_id"])
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
class_name KnownEventStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
|
||||
var data: Dictionary
|
||||
|
||||
|
||||
func _init(record_data: Dictionary = {}) -> void:
|
||||
data = record_data.duplicate(true)
|
||||
|
||||
|
||||
static func create(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
||||
return (
|
||||
KnownEventStateRecord
|
||||
. new(
|
||||
{
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"knower_id": knower_id,
|
||||
"event_id": event_id,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> KnownEventStateRecord:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if not record_data.has_all(["knower_id", "event_id"]):
|
||||
return null
|
||||
var knower_id := int(record_data["knower_id"])
|
||||
var event_id := int(record_data["event_id"])
|
||||
if knower_id < 0 or event_id < 0:
|
||||
return null
|
||||
return create(knower_id, event_id)
|
||||
|
||||
|
||||
func get_knower_id() -> int:
|
||||
return int(data["knower_id"])
|
||||
|
||||
|
||||
func get_event_id() -> int:
|
||||
return int(data["event_id"])
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return data.duplicate(true)
|
||||
@@ -0,0 +1 @@
|
||||
uid://n65oueyvbeoi
|
||||
@@ -1,9 +1,10 @@
|
||||
class_name NPCStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 3
|
||||
const SCHEMA_VERSION := 4
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
const PREVIOUS_SCHEMA_VERSION := 2
|
||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||
|
||||
var data: Dictionary
|
||||
|
||||
@@ -46,7 +47,6 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
|
||||
"last_task": String(npc.last_task),
|
||||
"random_seed": str(npc.random_source.seed),
|
||||
"random_state": str(npc.random_source.state),
|
||||
"familiarity": _sorted_familiarity(npc.familiarity),
|
||||
"mourning_ticks": npc.mourning_ticks
|
||||
}
|
||||
)
|
||||
@@ -54,7 +54,10 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> NPCStateRecord:
|
||||
var version := int(record_data.get("schema_version", -1))
|
||||
if version in [LEGACY_SCHEMA_VERSION, PREVIOUS_SCHEMA_VERSION]:
|
||||
if (
|
||||
version
|
||||
in [LEGACY_SCHEMA_VERSION, PREVIOUS_SCHEMA_VERSION, RELATIONSHIP_LEGACY_SCHEMA_VERSION]
|
||||
):
|
||||
record_data = _migrate_legacy(record_data, version)
|
||||
elif version != SCHEMA_VERSION:
|
||||
return null
|
||||
@@ -116,7 +119,9 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
||||
if version == LEGACY_SCHEMA_VERSION:
|
||||
migrated["travel_target_position"] = legacy_data.get("position", [0.0, 0.0, 0.0])
|
||||
migrated["has_travel_target"] = false
|
||||
migrated["inventory"] = {}
|
||||
if version in [LEGACY_SCHEMA_VERSION, PREVIOUS_SCHEMA_VERSION]:
|
||||
migrated["inventory"] = {}
|
||||
migrated.erase("familiarity")
|
||||
return migrated
|
||||
|
||||
|
||||
@@ -141,9 +146,7 @@ func restore(debug_logs: bool) -> SimNPC:
|
||||
float(saved_position[0]), float(saved_position[1]), float(saved_position[2])
|
||||
)
|
||||
var saved_home: Array = data.get("home_position", saved_position)
|
||||
npc.home_position = Vector3(
|
||||
float(saved_home[0]), float(saved_home[1]), float(saved_home[2])
|
||||
)
|
||||
npc.home_position = Vector3(float(saved_home[0]), float(saved_home[1]), float(saved_home[2]))
|
||||
npc.is_starving = bool(data["is_starving"])
|
||||
npc.starvation_ticks = int(data["starvation_ticks"])
|
||||
npc.starvation_death_threshold = int(data["starvation_death_threshold"])
|
||||
@@ -162,28 +165,9 @@ func restore(debug_logs: bool) -> SimNPC:
|
||||
npc.last_task = StringName(data["last_task"])
|
||||
npc.random_source.state = String(data["random_state"]).to_int()
|
||||
npc.debug_logs = debug_logs
|
||||
var saved_familiarity = data.get("familiarity", {})
|
||||
if saved_familiarity is Array:
|
||||
for pair in saved_familiarity:
|
||||
var pair_dict: Dictionary = pair
|
||||
npc.familiarity[int(pair_dict["id"])] = float(pair_dict["score"])
|
||||
elif saved_familiarity is Dictionary:
|
||||
npc.familiarity = saved_familiarity.duplicate(true)
|
||||
npc.mourning_ticks = int(data.get("mourning_ticks", 0))
|
||||
return npc
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return data.duplicate(true)
|
||||
|
||||
|
||||
static func _sorted_familiarity(familiarity: Dictionary) -> Array:
|
||||
var pairs: Array[Dictionary] = []
|
||||
for key in familiarity:
|
||||
pairs.append({"id": int(key), "score": float(familiarity[key])})
|
||||
pairs.sort_custom(_familiarity_sort)
|
||||
return pairs
|
||||
|
||||
|
||||
static func _familiarity_sort(a: Dictionary, b: Dictionary) -> bool:
|
||||
return int(a["id"]) < int(b["id"])
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
class_name RelationshipStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const NO_CAUSE_EVENT := -1
|
||||
const NEUTRAL_TRUST := 0.5
|
||||
|
||||
var data: Dictionary
|
||||
|
||||
|
||||
func _init(record_data: Dictionary = {}) -> void:
|
||||
data = record_data.duplicate(true)
|
||||
|
||||
|
||||
static func create(
|
||||
observer_id: int,
|
||||
subject_id: int,
|
||||
familiarity: float,
|
||||
trust: float = NEUTRAL_TRUST,
|
||||
last_trust_cause_event_id: int = NO_CAUSE_EVENT
|
||||
) -> RelationshipStateRecord:
|
||||
return (
|
||||
RelationshipStateRecord
|
||||
. new(
|
||||
{
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"observer_id": observer_id,
|
||||
"subject_id": subject_id,
|
||||
"familiarity": clampf(familiarity, 0.0, 1.0),
|
||||
"trust": clampf(trust, 0.0, 1.0),
|
||||
"last_trust_cause_event_id": last_trust_cause_event_id,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> RelationshipStateRecord:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if not record_data.has_all(
|
||||
["observer_id", "subject_id", "familiarity", "trust", "last_trust_cause_event_id"]
|
||||
):
|
||||
return null
|
||||
var observer_id := int(record_data["observer_id"])
|
||||
var subject_id := int(record_data["subject_id"])
|
||||
var familiarity := float(record_data["familiarity"])
|
||||
var trust := float(record_data["trust"])
|
||||
var cause_event_id := int(record_data["last_trust_cause_event_id"])
|
||||
if observer_id < 0 or subject_id < 0 or observer_id == subject_id:
|
||||
return null
|
||||
if familiarity < 0.0 or familiarity > 1.0 or trust < 0.0 or trust > 1.0:
|
||||
return null
|
||||
if cause_event_id < NO_CAUSE_EVENT:
|
||||
return null
|
||||
return create(observer_id, subject_id, familiarity, trust, cause_event_id)
|
||||
|
||||
|
||||
func get_observer_id() -> int:
|
||||
return int(data["observer_id"])
|
||||
|
||||
|
||||
func get_subject_id() -> int:
|
||||
return int(data["subject_id"])
|
||||
|
||||
|
||||
func get_familiarity() -> float:
|
||||
return float(data["familiarity"])
|
||||
|
||||
|
||||
func get_trust() -> float:
|
||||
return float(data["trust"])
|
||||
|
||||
|
||||
func get_last_trust_cause_event_id() -> int:
|
||||
return int(data["last_trust_cause_event_id"])
|
||||
|
||||
|
||||
func increase_familiarity(amount: float) -> float:
|
||||
var previous := get_familiarity()
|
||||
data["familiarity"] = clampf(previous + maxf(amount, 0.0), 0.0, 1.0)
|
||||
return get_familiarity() - previous
|
||||
|
||||
|
||||
func increase_trust(amount: float, cause_event_id: int) -> float:
|
||||
var previous := get_trust()
|
||||
data["trust"] = clampf(previous + maxf(amount, 0.0), 0.0, 1.0)
|
||||
var applied := get_trust() - previous
|
||||
if applied > 0.0:
|
||||
data["last_trust_cause_event_id"] = cause_event_id
|
||||
return applied
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return data.duplicate(true)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxvc6flu3fdka
|
||||
@@ -2,9 +2,11 @@ class_name SimulationStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_NAME := "the_steward.simulation"
|
||||
const SCHEMA_VERSION := 3
|
||||
const SCHEMA_VERSION := 5
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
const PREVIOUS_SCHEMA_VERSION := 2
|
||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||
const PREVIOUS_SCHEMA_VERSION := 4
|
||||
|
||||
var simulation: Dictionary
|
||||
var village: VillageStateRecord
|
||||
@@ -12,6 +14,8 @@ var npcs: Array[NPCStateRecord] = []
|
||||
var resources: Array[ResourceStateRecord] = []
|
||||
var storages: Array[StorageStateRecord] = []
|
||||
var economic_events: Array[EconomicEventRecord] = []
|
||||
var relationships: Array[RelationshipStateRecord] = []
|
||||
var event_knowledge: Array[KnownEventStateRecord] = []
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
@@ -28,6 +32,12 @@ func to_dictionary() -> Dictionary:
|
||||
var event_data: Array[Dictionary] = []
|
||||
for event_record in economic_events:
|
||||
event_data.append(event_record.to_dictionary())
|
||||
var relationship_data: Array[Dictionary] = []
|
||||
for relationship_record in relationships:
|
||||
relationship_data.append(relationship_record.to_dictionary())
|
||||
var knowledge_data: Array[Dictionary] = []
|
||||
for known_event_record in event_knowledge:
|
||||
knowledge_data.append(known_event_record.to_dictionary())
|
||||
|
||||
return {
|
||||
"schema": SCHEMA_NAME,
|
||||
@@ -37,7 +47,9 @@ func to_dictionary() -> Dictionary:
|
||||
"npcs": npc_data,
|
||||
"resources": resource_data,
|
||||
"storages": storage_data,
|
||||
"economic_events": event_data
|
||||
"economic_events": event_data,
|
||||
"relationships": relationship_data,
|
||||
"event_knowledge": knowledge_data
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +68,32 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
if record_data.get("schema", "") != SCHEMA_NAME:
|
||||
return null
|
||||
var version := int(record_data.get("schema_version", -1))
|
||||
if version in [LEGACY_SCHEMA_VERSION, PREVIOUS_SCHEMA_VERSION]:
|
||||
if (
|
||||
version
|
||||
in [
|
||||
LEGACY_SCHEMA_VERSION,
|
||||
EVENT_LEGACY_SCHEMA_VERSION,
|
||||
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
||||
PREVIOUS_SCHEMA_VERSION,
|
||||
]
|
||||
):
|
||||
record_data = _migrate_legacy(record_data, version)
|
||||
elif version != SCHEMA_VERSION:
|
||||
return null
|
||||
if not record_data.has_all(
|
||||
["simulation", "village", "npcs", "resources", "storages", "economic_events"]
|
||||
if not (
|
||||
record_data
|
||||
. has_all(
|
||||
[
|
||||
"simulation",
|
||||
"village",
|
||||
"npcs",
|
||||
"resources",
|
||||
"storages",
|
||||
"economic_events",
|
||||
"relationships",
|
||||
"event_knowledge",
|
||||
]
|
||||
)
|
||||
):
|
||||
return null
|
||||
|
||||
@@ -100,11 +132,15 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
var resource_data = record_data["resources"]
|
||||
var storage_data = record_data["storages"]
|
||||
var event_data = record_data["economic_events"]
|
||||
var relationship_data = record_data["relationships"]
|
||||
var knowledge_data = record_data["event_knowledge"]
|
||||
if (
|
||||
not npc_data is Array
|
||||
or not resource_data is Array
|
||||
or not storage_data is Array
|
||||
or not event_data is Array
|
||||
or not relationship_data is Array
|
||||
or not knowledge_data is Array
|
||||
):
|
||||
return null
|
||||
|
||||
@@ -152,6 +188,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
record.storages.append(storage_record)
|
||||
|
||||
var event_ids := {}
|
||||
var event_records_by_id := {}
|
||||
var highest_event_id := -1
|
||||
for item in event_data:
|
||||
if not item is Dictionary:
|
||||
@@ -163,11 +200,69 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
if event_ids.has(event_id):
|
||||
return null
|
||||
event_ids[event_id] = true
|
||||
event_records_by_id[event_id] = event_record
|
||||
highest_event_id = maxi(highest_event_id, event_id)
|
||||
record.economic_events.append(event_record)
|
||||
if int(record.simulation["next_event_id"]) <= highest_event_id:
|
||||
return null
|
||||
|
||||
var knowledge_keys := {}
|
||||
for item in knowledge_data:
|
||||
if not item is Dictionary:
|
||||
return null
|
||||
var known_event_record := KnownEventStateRecord.from_dictionary(item)
|
||||
if known_event_record == null:
|
||||
return null
|
||||
var knower_id := known_event_record.get_knower_id()
|
||||
var known_event_id := known_event_record.get_event_id()
|
||||
if not npc_ids.has(knower_id) or not event_ids.has(known_event_id):
|
||||
return null
|
||||
var knowledge_key := "%d:%d" % [knower_id, known_event_id]
|
||||
if knowledge_keys.has(knowledge_key):
|
||||
return null
|
||||
knowledge_keys[knowledge_key] = true
|
||||
record.event_knowledge.append(known_event_record)
|
||||
|
||||
var relationship_keys := {}
|
||||
for item in relationship_data:
|
||||
if not item is Dictionary:
|
||||
return null
|
||||
var relationship_record := RelationshipStateRecord.from_dictionary(item)
|
||||
if relationship_record == null:
|
||||
return null
|
||||
var observer_id := relationship_record.get_observer_id()
|
||||
var subject_id := relationship_record.get_subject_id()
|
||||
if not npc_ids.has(observer_id) or not npc_ids.has(subject_id):
|
||||
return null
|
||||
var relationship_key := "%d:%d" % [observer_id, subject_id]
|
||||
if relationship_keys.has(relationship_key):
|
||||
return null
|
||||
var cause_event_id := relationship_record.get_last_trust_cause_event_id()
|
||||
if (
|
||||
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
|
||||
and not event_ids.has(cause_event_id)
|
||||
):
|
||||
return null
|
||||
if (
|
||||
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
|
||||
and not knowledge_keys.has("%d:%d" % [observer_id, cause_event_id])
|
||||
):
|
||||
return null
|
||||
if cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT:
|
||||
var cause_event := event_records_by_id[cause_event_id] as EconomicEventRecord
|
||||
if (
|
||||
int(cause_event.data["actor_id"]) != subject_id
|
||||
or (
|
||||
StringName(cause_event.data["event_type"])
|
||||
!= SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
)
|
||||
or StringName(cause_event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
|
||||
or float(cause_event.data["amount"]) <= 0.0
|
||||
):
|
||||
return null
|
||||
relationship_keys[relationship_key] = true
|
||||
record.relationships.append(relationship_record)
|
||||
|
||||
return record
|
||||
|
||||
|
||||
@@ -196,8 +291,89 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
migrated["economic_events"] = []
|
||||
var simulation_data: Dictionary = migrated.get("simulation", {})
|
||||
simulation_data["next_event_id"] = 0
|
||||
migrated["simulation"] = simulation_data
|
||||
if version in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION]:
|
||||
migrated["economic_events"] = []
|
||||
var simulation_data: Dictionary = migrated.get("simulation", {})
|
||||
simulation_data["next_event_id"] = 0
|
||||
migrated["simulation"] = simulation_data
|
||||
if (
|
||||
version
|
||||
in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION, RELATIONSHIP_LEGACY_SCHEMA_VERSION]
|
||||
):
|
||||
migrated["relationships"] = _migrate_npc_familiarity(legacy_data.get("npcs", []))
|
||||
migrated["event_knowledge"] = _migrate_relationship_causes(migrated.get("relationships", []))
|
||||
return migrated
|
||||
|
||||
|
||||
static func _migrate_npc_familiarity(npc_data: Variant) -> Array[Dictionary]:
|
||||
var migrated_relationships: Array[Dictionary] = []
|
||||
if not npc_data is Array:
|
||||
return migrated_relationships
|
||||
for npc_item in npc_data:
|
||||
if not npc_item is Dictionary:
|
||||
continue
|
||||
var observer_id := int(npc_item.get("id", -1))
|
||||
var familiarity = npc_item.get("familiarity", [])
|
||||
if familiarity is Array:
|
||||
for pair in familiarity:
|
||||
if not pair is Dictionary:
|
||||
continue
|
||||
migrated_relationships.append(
|
||||
(
|
||||
RelationshipStateRecord
|
||||
. create(
|
||||
observer_id, int(pair.get("id", -1)), float(pair.get("score", 0.0))
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
)
|
||||
elif familiarity is Dictionary:
|
||||
for subject_id in familiarity:
|
||||
migrated_relationships.append(
|
||||
(
|
||||
RelationshipStateRecord
|
||||
. create(observer_id, int(subject_id), float(familiarity[subject_id]))
|
||||
. to_dictionary()
|
||||
)
|
||||
)
|
||||
migrated_relationships.sort_custom(_relationship_dictionary_sort)
|
||||
return migrated_relationships
|
||||
|
||||
|
||||
static func _relationship_dictionary_sort(first: Dictionary, second: Dictionary) -> bool:
|
||||
if int(first["observer_id"]) != int(second["observer_id"]):
|
||||
return int(first["observer_id"]) < int(second["observer_id"])
|
||||
return int(first["subject_id"]) < int(second["subject_id"])
|
||||
|
||||
|
||||
static func _migrate_relationship_causes(relationship_data: Variant) -> Array[Dictionary]:
|
||||
var migrated_knowledge: Array[Dictionary] = []
|
||||
var known_keys := {}
|
||||
if not relationship_data is Array:
|
||||
return migrated_knowledge
|
||||
for relationship_item in relationship_data:
|
||||
if not relationship_item is Dictionary:
|
||||
continue
|
||||
var observer_id := int(relationship_item.get("observer_id", -1))
|
||||
var cause_event_id := int(
|
||||
relationship_item.get(
|
||||
"last_trust_cause_event_id", RelationshipStateRecord.NO_CAUSE_EVENT
|
||||
)
|
||||
)
|
||||
if observer_id < 0 or cause_event_id < 0:
|
||||
continue
|
||||
var knowledge_key := "%d:%d" % [observer_id, cause_event_id]
|
||||
if known_keys.has(knowledge_key):
|
||||
continue
|
||||
known_keys[knowledge_key] = true
|
||||
migrated_knowledge.append(
|
||||
KnownEventStateRecord.create(observer_id, cause_event_id).to_dictionary()
|
||||
)
|
||||
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
||||
return migrated_knowledge
|
||||
|
||||
|
||||
static func _knowledge_dictionary_sort(first: Dictionary, second: Dictionary) -> bool:
|
||||
if int(first["knower_id"]) != int(second["knower_id"]):
|
||||
return int(first["knower_id"]) < int(second["knower_id"])
|
||||
return int(first["event_id"]) < int(second["event_id"])
|
||||
|
||||
Reference in New Issue
Block a user