feat: derive capable opportunity helpers
This commit is contained in:
@@ -806,23 +806,32 @@ func get_active_opportunity() -> OpportunityStateRecord:
|
||||
return opportunity_system.get_open_opportunity()
|
||||
|
||||
|
||||
func get_active_opportunity_helper() -> OpportunityHelperResult:
|
||||
var active := get_active_opportunity()
|
||||
if active == null:
|
||||
return null
|
||||
return opportunity_system.find_capable_helper(
|
||||
active,
|
||||
economy.get_storage(active.get_target_id()),
|
||||
npcs,
|
||||
event_knowledge_system,
|
||||
relationship_system,
|
||||
resource_states
|
||||
)
|
||||
|
||||
|
||||
func get_latest_opportunity_for_npc(npc_id: int) -> OpportunityStateRecord:
|
||||
return opportunity_system.get_latest_for_npc(npc_id)
|
||||
|
||||
|
||||
func get_opportunity_trigger_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
||||
if opportunity == null:
|
||||
return null
|
||||
return event_log.get_by_id(opportunity.get_trigger_event_id())
|
||||
var event_id := opportunity.get_trigger_event_id() if opportunity != null else -1
|
||||
return event_log.get_by_id(event_id)
|
||||
|
||||
|
||||
func get_opportunity_resolution_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
||||
if (
|
||||
opportunity == null
|
||||
or opportunity.get_resolution_event_id() == OpportunityStateRecord.NO_EVENT_ID
|
||||
):
|
||||
return null
|
||||
return event_log.get_by_id(opportunity.get_resolution_event_id())
|
||||
var event_id := opportunity.get_resolution_event_id() if opportunity != null else -1
|
||||
return event_log.get_by_id(event_id)
|
||||
|
||||
|
||||
func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
||||
@@ -1106,7 +1115,6 @@ func create_state_record() -> SimulationStateRecord:
|
||||
wander_streams.append(
|
||||
{"npc_id": int(npc_id), "seed": str(source.seed), "state": str(source.state)}
|
||||
)
|
||||
|
||||
record.simulation = {
|
||||
"seed": simulation_seed,
|
||||
"tick_interval": tick_interval,
|
||||
@@ -1121,7 +1129,6 @@ func create_state_record() -> SimulationStateRecord:
|
||||
record.village = VillageStateRecord.capture(village)
|
||||
for npc in npcs:
|
||||
record.npcs.append(NPCStateRecord.capture(npc))
|
||||
|
||||
var sorted_resource_ids: Array = resource_states.keys()
|
||||
sorted_resource_ids.sort()
|
||||
for resource_id in sorted_resource_ids:
|
||||
@@ -1158,7 +1165,6 @@ func restore_state_from_json(json_text: String) -> bool:
|
||||
func restore_state(record: SimulationStateRecord) -> bool:
|
||||
if record == null:
|
||||
return false
|
||||
|
||||
simulation_seed = int(record.simulation["seed"])
|
||||
tick_interval = float(record.simulation["tick_interval"])
|
||||
tick_count = int(record.simulation["tick_count"])
|
||||
@@ -1171,7 +1177,6 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
economy.restore_storage(record.storages)
|
||||
event_log.restore(record.economic_events, int(record.simulation["next_event_id"]))
|
||||
latest_decisions.clear()
|
||||
|
||||
npcs.clear()
|
||||
for npc_record in record.npcs:
|
||||
npcs.append(npc_record.restore(debug_logs))
|
||||
@@ -1179,7 +1184,6 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
event_knowledge_system.restore(record.event_knowledge)
|
||||
opportunity_system.restore(record.opportunities, int(record.simulation["next_opportunity_id"]))
|
||||
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
|
||||
|
||||
wander_random_sources.clear()
|
||||
var wander_streams: Array = record.simulation["wander_random_streams"]
|
||||
for stream_data in wander_streams:
|
||||
@@ -1187,12 +1191,10 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
source.seed = String(stream_data["seed"]).to_int()
|
||||
source.state = String(stream_data["state"]).to_int()
|
||||
wander_random_sources[int(stream_data["npc_id"])] = source
|
||||
|
||||
resource_states.clear()
|
||||
for resource_record in record.resources:
|
||||
resource_states[resource_record.get_node_id()] = resource_record
|
||||
register_loaded_resource_nodes()
|
||||
|
||||
village_changed.emit(village)
|
||||
state_restored.emit()
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class_name OpportunityHelperResult
|
||||
extends RefCounted
|
||||
|
||||
var opportunity_id: int
|
||||
var helper_npc_id: int
|
||||
var action_id: StringName
|
||||
var source_id: StringName
|
||||
var resource_id: StringName
|
||||
var trigger_event_id: int
|
||||
var trust: float
|
||||
var familiarity: float
|
||||
var uses_inventory: bool
|
||||
var available_source_count: int
|
||||
var profession_match: bool
|
||||
var reason: String
|
||||
|
||||
|
||||
func _init(result: Dictionary) -> void:
|
||||
opportunity_id = int(result["opportunity_id"])
|
||||
helper_npc_id = int(result["helper_npc_id"])
|
||||
action_id = StringName(result["action_id"])
|
||||
source_id = StringName(result["source_id"])
|
||||
resource_id = StringName(result["resource_id"])
|
||||
trigger_event_id = int(result["trigger_event_id"])
|
||||
trust = float(result["trust"])
|
||||
familiarity = float(result["familiarity"])
|
||||
uses_inventory = bool(result["uses_inventory"])
|
||||
available_source_count = int(result["available_source_count"])
|
||||
profession_match = bool(result["profession_match"])
|
||||
reason = String(result["reason"])
|
||||
@@ -0,0 +1 @@
|
||||
uid://o8t4og3iq8dv
|
||||
@@ -112,6 +112,196 @@ func get_latest_for_npc(npc_id: int) -> OpportunityStateRecord:
|
||||
return null
|
||||
|
||||
|
||||
func find_capable_helper(
|
||||
opportunity: OpportunityStateRecord,
|
||||
target_storage: StorageStateRecord,
|
||||
npcs: Array[SimNPC],
|
||||
knowledge_system: EventKnowledgeSystem,
|
||||
relationship_system: RefCounted,
|
||||
resource_states: Dictionary
|
||||
) -> OpportunityHelperResult:
|
||||
if (
|
||||
opportunity == null
|
||||
or not opportunity.is_open()
|
||||
or target_storage == null
|
||||
or knowledge_system == null
|
||||
or relationship_system == null
|
||||
):
|
||||
return null
|
||||
var interested := _find_npc(opportunity.get_interested_npc_id(), npcs)
|
||||
if interested == null or interested.is_dead:
|
||||
return null
|
||||
var remaining := maxf(
|
||||
opportunity.get_target_amount() - target_storage.get_amount(opportunity.get_resource_id()),
|
||||
0.0
|
||||
)
|
||||
if remaining <= 0.0 or target_storage.get_available_capacity() < remaining:
|
||||
return null
|
||||
var action_ids := _get_supply_action_ids(opportunity.get_resource_id())
|
||||
if action_ids.is_empty():
|
||||
return null
|
||||
var gather_definition := SimulationDefinitions.get_action(action_ids["gather"])
|
||||
var deposit_definition := SimulationDefinitions.get_action(action_ids["deposit"])
|
||||
if gather_definition == null or deposit_definition == null:
|
||||
return null
|
||||
var candidates: Array[Dictionary] = []
|
||||
for npc in npcs:
|
||||
var candidate := _build_helper_candidate(
|
||||
npc,
|
||||
opportunity,
|
||||
remaining,
|
||||
gather_definition,
|
||||
deposit_definition,
|
||||
interested.npc_name,
|
||||
knowledge_system,
|
||||
relationship_system,
|
||||
resource_states
|
||||
)
|
||||
if not candidate.is_empty():
|
||||
candidates.append(candidate)
|
||||
if candidates.is_empty():
|
||||
return null
|
||||
candidates.sort_custom(_sort_helper_candidates)
|
||||
var best := candidates[0]
|
||||
return (
|
||||
OpportunityHelperResult
|
||||
. new(
|
||||
{
|
||||
"opportunity_id": opportunity.get_opportunity_id(),
|
||||
"helper_npc_id": (best["npc"] as SimNPC).id,
|
||||
"action_id": best["action_id"],
|
||||
"source_id": best["source_id"],
|
||||
"resource_id": opportunity.get_resource_id(),
|
||||
"trigger_event_id": opportunity.get_trigger_event_id(),
|
||||
"trust": best["trust"],
|
||||
"familiarity": best["familiarity"],
|
||||
"uses_inventory": best["uses_inventory"],
|
||||
"available_source_count": best["available_source_count"],
|
||||
"profession_match": best["profession_match"],
|
||||
"reason": best["reason"],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _build_helper_candidate(
|
||||
npc: SimNPC,
|
||||
opportunity: OpportunityStateRecord,
|
||||
remaining: float,
|
||||
gather_definition: ActionDefinition,
|
||||
deposit_definition: ActionDefinition,
|
||||
interested_name: String,
|
||||
knowledge_system: EventKnowledgeSystem,
|
||||
relationship_system: RefCounted,
|
||||
resource_states: Dictionary
|
||||
) -> Dictionary:
|
||||
if npc.is_dead or npc.id == opportunity.get_interested_npc_id():
|
||||
return {}
|
||||
if not knowledge_system.knows_event(npc.id, opportunity.get_trigger_event_id()):
|
||||
return {}
|
||||
var relationship: RelationshipStateRecord = relationship_system.get_relationship(
|
||||
npc.id, opportunity.get_interested_npc_id()
|
||||
)
|
||||
if (
|
||||
relationship == null
|
||||
or not relationship_system.is_trusted_for_help(npc.id, opportunity.get_interested_npc_id())
|
||||
):
|
||||
return {}
|
||||
var carried := npc.get_inventory_amount(opportunity.get_resource_id())
|
||||
var uses_inventory := carried >= remaining
|
||||
var available_source_count := 0
|
||||
if not uses_inventory:
|
||||
available_source_count = _count_available_resource_sources(
|
||||
npc.id, remaining, opportunity.get_resource_id(), gather_definition, resource_states
|
||||
)
|
||||
if available_source_count == 0:
|
||||
return {}
|
||||
var action_definition := deposit_definition if uses_inventory else gather_definition
|
||||
var source_id := SimulationIds.npc_inventory_id(npc.id) if uses_inventory else &""
|
||||
var profession_match := (
|
||||
not gather_definition.preferred_profession_id.is_empty()
|
||||
and npc.profession == gather_definition.preferred_profession_id
|
||||
)
|
||||
var supply_fact := (
|
||||
"carries %.1f %s" % [carried, String(opportunity.get_resource_id()).capitalize()]
|
||||
if uses_inventory
|
||||
else (
|
||||
"has %d available finite %s source%s"
|
||||
% [
|
||||
available_source_count,
|
||||
String(opportunity.get_resource_id()).capitalize(),
|
||||
"" if available_source_count == 1 else "s",
|
||||
]
|
||||
)
|
||||
)
|
||||
return {
|
||||
"npc": npc,
|
||||
"action_id": action_definition.action_id,
|
||||
"source_id": source_id,
|
||||
"trust": relationship.get_trust(),
|
||||
"familiarity": relationship.get_familiarity(),
|
||||
"uses_inventory": uses_inventory,
|
||||
"available_source_count": available_source_count,
|
||||
"profession_match": profession_match,
|
||||
"reason":
|
||||
(
|
||||
"Knows the need; trust %.2f toward %s; %s"
|
||||
% [relationship.get_trust(), interested_name, supply_fact]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
static func _get_supply_action_ids(resource_id: StringName) -> Dictionary:
|
||||
match resource_id:
|
||||
SimulationIds.RESOURCE_FOOD:
|
||||
return {
|
||||
"gather": SimulationIds.ACTION_GATHER_FOOD,
|
||||
"deposit": SimulationIds.ACTION_DEPOSIT_FOOD,
|
||||
}
|
||||
SimulationIds.RESOURCE_WOOD:
|
||||
return {
|
||||
"gather": SimulationIds.ACTION_GATHER_WOOD,
|
||||
"deposit": SimulationIds.ACTION_DEPOSIT_WOOD,
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
static func _count_available_resource_sources(
|
||||
npc_id: int,
|
||||
remaining: float,
|
||||
resource_id: StringName,
|
||||
gather_definition: ActionDefinition,
|
||||
resource_states: Dictionary
|
||||
) -> int:
|
||||
var count := 0
|
||||
for value in resource_states.values():
|
||||
var state := value as ResourceStateRecord
|
||||
if (
|
||||
state == null
|
||||
or state.get_action_id() != gather_definition.resource_action_id
|
||||
or state.get_resource_id() != resource_id
|
||||
or not state.can_npc_use()
|
||||
or not state.is_available_for(npc_id)
|
||||
or state.get_yield_per_action() <= 0.0
|
||||
or state.get_amount_remaining() < remaining
|
||||
):
|
||||
continue
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
static func _sort_helper_candidates(first: Dictionary, second: Dictionary) -> bool:
|
||||
if first["uses_inventory"] != second["uses_inventory"]:
|
||||
return bool(first["uses_inventory"])
|
||||
if first["trust"] != second["trust"]:
|
||||
return float(first["trust"]) > float(second["trust"])
|
||||
if first["profession_match"] != second["profession_match"]:
|
||||
return bool(first["profession_match"])
|
||||
if first["familiarity"] != second["familiarity"]:
|
||||
return float(first["familiarity"]) > float(second["familiarity"])
|
||||
return (first["npc"] as SimNPC).id < (second["npc"] as SimNPC).id
|
||||
|
||||
|
||||
func _try_open_food(
|
||||
event: EconomicEventRecord,
|
||||
current_tick: int,
|
||||
|
||||
@@ -38,6 +38,15 @@ func get_relationship(observer_id: int, subject_id: int) -> RelationshipStateRec
|
||||
return relationships.get(_key(observer_id, subject_id)) as RelationshipStateRecord
|
||||
|
||||
|
||||
func is_trusted_for_help(observer_id: int, subject_id: int) -> bool:
|
||||
var relationship := get_relationship(observer_id, subject_id)
|
||||
return (
|
||||
relationship != null
|
||||
and relationship.get_familiarity() > 0.0
|
||||
and relationship.get_trust() >= TRUSTED_HELP_THRESHOLD
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -66,7 +66,21 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
|
||||
var action_id := StringName(record_data["action_id"])
|
||||
if not action_id.is_empty() and SimulationDefinitions.get_action(action_id) == null:
|
||||
return null
|
||||
return ResourceStateRecord.new(record_data)
|
||||
var normalized := record_data.duplicate(true)
|
||||
normalized["schema_version"] = SCHEMA_VERSION
|
||||
normalized["node_id"] = String(record_data["node_id"])
|
||||
normalized["action_id"] = String(record_data["action_id"])
|
||||
normalized["resource_id"] = String(record_data["resource_id"])
|
||||
normalized["amount_remaining"] = float(record_data["amount_remaining"])
|
||||
normalized["yield_per_action"] = float(record_data["yield_per_action"])
|
||||
normalized["reserved_by"] = int(record_data["reserved_by"])
|
||||
normalized["enabled"] = bool(record_data["enabled"])
|
||||
normalized["can_npcs_use"] = bool(record_data["can_npcs_use"])
|
||||
normalized["can_player_use"] = bool(record_data["can_player_use"])
|
||||
normalized["safety_risk"] = float(record_data["safety_risk"])
|
||||
normalized["comfort_distance"] = float(record_data["comfort_distance"])
|
||||
normalized["discovery_priority"] = float(record_data["discovery_priority"])
|
||||
return ResourceStateRecord.new(normalized)
|
||||
|
||||
|
||||
static func _migrate_v1(legacy_data: Dictionary) -> Dictionary:
|
||||
|
||||
Reference in New Issue
Block a user