feat: derive capable opportunity helpers

This commit is contained in:
Rijad Zuzo
2026-07-16 10:41:05 +02:00
parent 79dd503343
commit eaa15c076c
17 changed files with 648 additions and 40 deletions
@@ -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,