feat: let opportunity helpers act autonomously

This commit is contained in:
Rijad Zuzo
2026-07-16 10:58:48 +02:00
parent eaa15c076c
commit 0332c57a89
14 changed files with 253 additions and 30 deletions
+8 -3
View File
@@ -5,6 +5,7 @@
```text ```text
SimulationManager tick SimulationManager tick
-> ActionExecutionSystem advances needs and working progress -> ActionExecutionSystem advances needs and working progress
-> VillageOpportunitySystem re-derives the current capable helper
-> ActionSelectionSystem chooses an action when the NPC is idle -> ActionSelectionSystem chooses an action when the NPC is idle
-> SimulationManager stores the selected action -> SimulationManager stores the selected action
-> npc_target_requested -> npc_target_requested
@@ -24,6 +25,9 @@ SimulationManager tick
- queries `RelationshipSystem` after personal survival/schedule overrides, so a - queries `RelationshipSystem` after personal survival/schedule overrides, so a
trusted starving acquaintance can redirect ordinary work toward food trusted starving acquaintance can redirect ordinary work toward food
gathering while the pantry is low; gathering while the pantry is low;
- accepts the current ephemeral `OpportunityHelperResult` and, only for its
matching NPC, selects the reported ordinary gather/deposit action after
urgent self-care and sleep/meal schedule branches;
- evaluates current utility scores; - evaluates current utility scores;
- consumes the NPC's deterministic decision RNG; - consumes the NPC's deterministic decision RNG;
- returns an action ID, optional urgent-duration override, branch reason, - returns an action ID, optional urgent-duration override, branch reason,
@@ -68,9 +72,10 @@ SimulationManager remains the orchestrator and event boundary. It owns
simulation records, invokes the focused systems, stores selected actions and simulation records, invokes the focused systems, stores selected actions and
targets, and translates presentation callbacks into simulation transitions. targets, and translates presentation callbacks into simulation transitions.
It also publishes the latest `ActionSelectionResult` for presentation; the UI It also publishes the latest `ActionSelectionResult` for presentation; the UI
does not recompute decisions. At completion it atomically pays any does not recompute decisions. Each idle selection re-derives the capable helper
definition-backed stored-resource cost before applying the action effect. A instead of consulting persisted assignment state. At completion it atomically
late shortfall suppresses the effect and records a `task_blocked` fact. The pays any definition-backed stored-resource cost before applying the action
effect. A late shortfall suppresses the effect and records a `task_blocked` fact. The
manager also captures actor/nearby knowledge before forwarding newly recorded manager also captures actor/nearby knowledge before forwarding newly recorded
events into `RelationshipSystem`. When an NPC arrives beside a worker at the events into `RelationshipSystem`. When an NPC arrives beside a worker at the
same non-storage activity site, the manager may transfer one direct known fact same non-storage activity site, the manager may transfer one direct known fact
+6 -2
View File
@@ -11,6 +11,7 @@ results.
SimulationClock SimulationClock
-> SimulationManager orchestrates one deterministic tick -> SimulationManager orchestrates one deterministic tick
-> ActionExecutionSystem advances needs and work -> ActionExecutionSystem advances needs and work
-> VillageOpportunitySystem re-derives the current capable helper
-> ActionSelectionSystem chooses an action -> ActionSelectionSystem chooses an action
-> ActionTargetResolver resolves a stable target ID -> ActionTargetResolver resolves a stable target ID
-> VillageEconomy performs inventory/storage transactions -> VillageEconomy performs inventory/storage transactions
@@ -45,7 +46,8 @@ would otherwise obscure that lifecycle:
open/resolved/invalidated lifecycle for the proven pantry-food and blocked- open/resolved/invalidated lifecycle for the proven pantry-food and blocked-
work wood consumers. It also derives one ephemeral `OpportunityHelperResult` work wood consumers. It also derives one ephemeral `OpportunityHelperResult`
from knowledge, directed relationships, inventory, action definitions, and from knowledge, directed relationships, inventory, action definitions, and
finite-resource state without changing resources or assigning tasks; finite-resource state without changing resources or assigning tasks. The
ordinary action selector consumes that result only for the matching idle NPC;
- `simulation/persistence/` owns save-slot file safety; - `simulation/persistence/` owns save-slot file safety;
- `simulation/state/` owns versioned serialized record contracts; - `simulation/state/` owns versioned serialized record contracts;
- `simulation/definitions/` owns stable IDs and immutable action/profession - `simulation/definitions/` owns stable IDs and immutable action/profession
@@ -103,7 +105,9 @@ improving ownership.
event IDs rather than object references or prose. event IDs rather than object references or prose.
- Opportunity records reference stable NPC, storage, resource, trigger-event, - Opportunity records reference stable NPC, storage, resource, trigger-event,
and resolution-event IDs. Their generator may observe authoritative state and resolution-event IDs. Their generator may observe authoritative state
and history, but it does not mutate the economy or command NPC behavior. and history, but it does not mutate the economy or command NPC behavior. The
manager re-queries capable helpers at idle selection boundaries; any selected
supply task then follows the ordinary persisted NPC-task and target contracts.
- Prefer one tested vertical behavior over a generic framework with no proven - Prefer one tested vertical behavior over a generic framework with no proven
consumers. consumers.
+10 -4
View File
@@ -736,13 +736,19 @@ Completed:
deposit action and real inventory/resource route that makes them capable. deposit action and real inventory/resource route that makes them capable.
The result is re-derived after restore and does not assign work or alter the The result is re-derived after restore and does not assign work or alter the
simulation. simulation.
29. Autonomous capable helper: the matching idle villager now consumes that
ephemeral result through ordinary action selection after urgent self-care
and sleep/meal precedence. Existing target resolution chooses and reserves
the finite source, while no quest acceptance, reward, or helper assignment
is introduced.
Next: Next:
1. Let the currently derived helper consume the query through ordinary 1. Prove one direct information-to-help chain: transfer the exact open-need
autonomous action selection, after urgent self-care and schedule precedence. trigger at an existing shared activity, then let the newly informed trusted
Re-query instead of persisting assignment, and keep acceptance/rewards out villager be re-derived and respond through the same autonomous selector.
of scope, as sequenced in `LEARNING_ROADMAP.md`. Keep generic dialogue and multi-hop rumours out of scope, as sequenced in
`LEARNING_ROADMAP.md`.
Do not start with GIS data, a full city, a large asset pack, or more NPC Do not start with GIS data, a full city, a large asset pack, or more NPC
mechanics. The next proof is a beautiful stage for the systems that already mechanics. The next proof is a beautiful stage for the systems that already
+13 -3
View File
@@ -112,6 +112,15 @@ does not select an exact gather target; that remains with `ActiveWorldAdapter`
and `ActionTargetResolver`. No event, reservation, RNG draw, task mutation, or and `ActionTargetResolver`. No event, reservation, RNG draw, task mutation, or
serialized assignment is created. serialized assignment is created.
At an idle NPC decision boundary, `SimulationManager` performs that query again
and gives the ephemeral result to `ActionSelectionSystem`. Personal survival,
low-energy rest, mourning, and sleep/meal schedule branches retain precedence;
otherwise only the named helper selects the reported ordinary gather/deposit
action. From there, existing target resolution chooses and reserves an exact
finite resource or storage target. The current NPC task may serialize normally
while in progress, but no opportunity-to-helper assignment or acceptance state
exists in the save.
The food-loop regression verifies this chain: The food-loop regression verifies this chain:
```text ```text
@@ -151,6 +160,7 @@ hearing, personalized reinforcement/decay, multi-hop rumours, secrecy, false
beliefs, and multi-event causal graphs belong in later event/history slices. beliefs, and multi-event causal graphs belong in later event/history slices.
They should extend this record family without making prose authoritative or They should extend this record family without making prose authoritative or
recomputing old evidence from current positions. recomputing old evidence from current positions.
The current opportunity family is likewise a bounded two-consumer projection The current opportunity family is likewise a bounded two-type projection with
with one read-only helper query, not a generic quest, reward, acceptance, one read-only helper query and one ordinary action-selection consumer, not a
dialogue, helper assignment, or capable-helper framework. generic quest, reward, acceptance, dialogue, helper assignment, or capable-
helper framework.
+22 -5
View File
@@ -792,11 +792,28 @@ The first bounded capable-helper query is complete:
- food and wood scenarios re-derive the same result after restore, and the - food and wood scenarios re-derive the same result after restore, and the
village summary explains the current helper route from real simulation facts. village summary explains the current helper route from real simulation facts.
Milestone 7 is not complete. The immediate next slice is one bounded autonomous The first bounded autonomous helper consumer is complete:
consumer of this query: let the currently derived helper choose the reported
gather/deposit action through ordinary selection while preserving urgent - when an NPC becomes idle, `SimulationManager` re-derives the current helper
self-care and schedule precedence. Re-query rather than persisting assignment, and passes that ephemeral result into ordinary action selection;
and do not add quest acceptance or reward state. - only the matching helper can select the reported gather/deposit action, and
starvation, critical hunger, mourning, low energy, sleep, and meal behavior
keep precedence;
- the resulting task uses the existing definition, target resolver, finite-node
reservation, travel, extraction, inventory, and storage transaction paths;
- no helper assignment, acceptance, reward, opportunity-specific event, or RNG
draw is added, while an in-progress ordinary task continues through the
existing NPC save fields and task-start history;
- focused headless and Jajce runtime regressions prove autonomous selection,
precedence, finite-source targeting, reservation, and restore-time re-query.
Milestone 7 is not complete. The immediate next slice should connect the
existing one-hop fact transfer to this path: prove that a direct witness or
performer can communicate an open need's exact trigger at a real shared
activity, after which the newly informed trusted villager is re-derived as the
helper and responds through the same autonomous selection contract. Keep this
as a truthful event-reference transfer, not generic dialogue or multi-hop
rumour infrastructure.
Recently completed: Recently completed:
+18 -6
View File
@@ -619,8 +619,10 @@ These are expected prototype constraints, not necessarily isolated bugs:
`supply_missing_wood` need from exact known evidence. Both have exact NPC and `supply_missing_wood` need from exact known evidence. Both have exact NPC and
player supply resolutions; the wood need also closes deterministically on player supply resolutions; the wood need also closes deterministically on
interested-party death or one-day staleness. One read-only query derives and interested-party death or one-day staleness. One read-only query derives and
explains a capable helper, but there is still no helper assignment, explains a capable helper, and the matching idle NPC can consume it through
acceptance, rewards, free-form dialogue, or quest log. ordinary action selection after urgent self-care and schedule precedence.
There is still no helper assignment, acceptance, rewards, free-form dialogue,
or quest log.
- The reason inspector exposes current decisions, utility rejections, one exact - The reason inspector exposes current decisions, utility rejections, one exact
relationship cause, and a compact person-history view that distinguishes relationship cause, and a compact person-history view that distinguishes
importance-ranked retained memories from objective personal actions. importance-ranked retained memories from objective personal actions.
@@ -911,10 +913,20 @@ produce one deterministic result. The village summary explains that result;
save/load re-derives it, and querying changes no task, reservation, RNG stream, save/load re-derives it, and querying changes no task, reservation, RNG stream,
resource, event, or checksum. resource, event, or checksum.
Milestone 7 remains in progress. The immediate next slice is one bounded The first autonomous helper consumer is complete. At each idle decision
autonomous consumer: allow the currently derived helper to choose its reported boundary the manager re-derives capability, and only the matching helper may
ordinary supply action after urgent self-care and schedule rules, without select the reported ordinary gather/deposit action. Urgent self-care and
persisted assignment, quest acceptance, or reward state. sleep/meal behavior still win; accepted supply work uses the existing target
resolver, finite-node reservation, travel, inventory, and transaction paths.
The ordinary in-progress NPC task can save normally, but no helper assignment,
quest acceptance, reward, or opportunity-specific event is persisted; ordinary
task-start history remains unchanged.
Milestone 7 remains in progress. The immediate next slice should prove the
combined information-to-help path: communicate an open need's exact trigger
once at a real shared activity, then re-derive the newly informed trusted NPC as
the autonomous helper. Keep the transfer direct and source-backed rather than
introducing generic dialogue or multi-hop rumours.
The remaining simulation-garden target still aims for: The remaining simulation-garden target still aims for:
+5
View File
@@ -202,6 +202,11 @@ relationship, inventory, action-definition, storage-capacity, and finite-
resource records. Save/restore regressions require the same helper/action/route resource records. Save/restore regressions require the same helper/action/route
result and an unchanged checksum before and after querying. result and an unchanged checksum before and after querying.
When an idle derived helper chooses the reported supply action, only the
existing NPC task, task-state, target, travel, and reservation fields serialize.
The helper result remains absent: restore retains any in-progress ordinary task
and independently re-derives current capability from restored facts.
## Resource authority ## Resource authority
`SimulationManager` owns `ResourceStateRecord` instances independently of the `SimulationManager` owns `ResourceStateRecord` instances independently of the
+2 -2
View File
@@ -156,7 +156,6 @@ func simulate_tick() -> void:
tick_count += 1 tick_count += 1
if debug_logs: if debug_logs:
print("--- Tick ", tick_count, " ---") print("--- Tick ", tick_count, " ---")
var village_was_changed := false var village_was_changed := false
for npc in npcs: for npc in npcs:
village_was_changed = _simulate_npc_tick(npc) or village_was_changed village_was_changed = _simulate_npc_tick(npc) or village_was_changed
@@ -207,7 +206,8 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
return return
if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]: if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
return return
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs) var helper := get_active_opportunity_helper()
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs, helper)
if selection == null: if selection == null:
return return
latest_decisions[npc.id] = selection latest_decisions[npc.id] = selection
+34 -1
View File
@@ -18,7 +18,11 @@ var relationship_system: RefCounted
func select_action( func select_action(
npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, all_npcs: Array = [] npc: SimNPC,
village: SimVillage,
time_of_day: float = 0.5,
all_npcs: Array = [],
opportunity_helper: OpportunityHelperResult = null
) -> ActionSelectionResult: ) -> ActionSelectionResult:
if npc.is_dead: if npc.is_dead:
return null return null
@@ -98,6 +102,17 @@ func select_action(
return ActionSelectionResult.new( return ActionSelectionResult.new(
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Hungry; pantry is empty" SimulationIds.ACTION_GATHER_FOOD, -1.0, "Hungry; pantry is empty"
) )
var is_opportunity_helper := _is_current_opportunity_helper(npc, opportunity_helper)
if is_opportunity_helper and npc.energy < 25.0:
return ActionSelectionResult.new(
SimulationIds.ACTION_REST, -1.0, "Energy is low; resting before helping"
)
if is_opportunity_helper:
return ActionSelectionResult.new(
opportunity_helper.action_id,
-1.0,
"Responding to village need: %s" % opportunity_helper.reason
)
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0: if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
return ActionSelectionResult.new( return ActionSelectionResult.new(
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage" SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
@@ -131,6 +146,24 @@ func select_action(
return _choose_best_work_action(npc, village) return _choose_best_work_action(npc, village)
func _is_current_opportunity_helper(
npc: SimNPC, opportunity_helper: OpportunityHelperResult
) -> bool:
return (
opportunity_helper != null
and opportunity_helper.helper_npc_id == npc.id
and (
opportunity_helper.action_id
in [
SimulationIds.ACTION_GATHER_FOOD,
SimulationIds.ACTION_GATHER_WOOD,
SimulationIds.ACTION_DEPOSIT_FOOD,
SimulationIds.ACTION_DEPOSIT_WOOD,
]
)
)
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult: func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
var scores := { var scores := {
SimulationIds.ACTION_GATHER_FOOD: SimulationIds.ACTION_GATHER_FOOD:
+74
View File
@@ -28,6 +28,7 @@ func _initialize() -> void:
func _run() -> void: func _run() -> void:
_test_selection_and_execution_are_separate() _test_selection_and_execution_are_separate()
_test_opportunity_helper_selection_preserves_precedence()
_test_target_resolution_and_travel_are_separate() _test_target_resolution_and_travel_are_separate()
if failures.is_empty(): if failures.is_empty():
@@ -91,6 +92,57 @@ func _test_selection_and_execution_are_separate() -> void:
) )
func _test_opportunity_helper_selection_preserves_precedence() -> void:
var village := SimVillage.new()
village.food = 100.0
village.wood = 100.0
village.safety = 100.0
village.knowledge = 100.0
village.update_modifiers()
village.update_priorities()
var npc := SimNPC.new(702, "OpportunityHelper", SimulationIds.PROFESSION_WANDERER, 5.0, 5.0)
npc.hunger = 20.0
npc.energy = 80.0
var helper := _helper_result(npc.id, SimulationIds.ACTION_GATHER_WOOD)
var selector := ActionSelectionSystem.new()
var selection := selector.select_action(npc, village, 0.5, [npc], helper)
_check(
(
selection.action_id == SimulationIds.ACTION_GATHER_WOOD
and "Responding to village need" in selection.reason
and "Knows the need" in selection.reason
),
"The matching derived helper should choose its reported ordinary supply action"
)
npc.energy = 20.0
selection = selector.select_action(npc, village, 0.5, [npc], helper)
_check(
selection.action_id == SimulationIds.ACTION_REST,
"Urgent low energy should take precedence over an opportunity response"
)
npc.energy = 50.0
selection = selector.select_action(npc, village, 0.0, [npc], helper)
_check(
selection.action_id == SimulationIds.ACTION_SLEEP,
"The sleep schedule should take precedence over an opportunity response"
)
npc.energy = 80.0
npc.hunger = 90.0
selection = selector.select_action(npc, village, 0.5, [npc], helper)
_check(
selection.action_id == SimulationIds.ACTION_WITHDRAW_FOOD,
"Critical hunger should take precedence over an opportunity response"
)
npc.hunger = 20.0
helper = _helper_result(npc.id + 1, SimulationIds.ACTION_GATHER_WOOD)
selection = selector.select_action(npc, village, 0.5, [npc], helper)
_check(
"Responding to village need" not in selection.reason,
"A helper result must not redirect a different NPC"
)
func _test_target_resolution_and_travel_are_separate() -> void: func _test_target_resolution_and_travel_are_separate() -> void:
var adapter := FakeActiveWorld.new() var adapter := FakeActiveWorld.new()
adapter.resource_candidates = [ adapter.resource_candidates = [
@@ -185,6 +237,28 @@ func _on_travel_requested(_npc: SimNPC, target_position: Vector3) -> void:
travel_requests.append(target_position) travel_requests.append(target_position)
func _helper_result(helper_id: int, action_id: StringName) -> OpportunityHelperResult:
return (
OpportunityHelperResult
. new(
{
"opportunity_id": 9,
"helper_npc_id": helper_id,
"action_id": action_id,
"source_id": "",
"resource_id": SimulationIds.RESOURCE_WOOD,
"trigger_event_id": 14,
"trust": 0.7,
"familiarity": 0.8,
"uses_inventory": false,
"available_source_count": 1,
"profession_match": false,
"reason": "Knows the need; trust 0.70 toward Neighbour; has 1 finite source",
}
)
)
func _check(condition: bool, message: String) -> void: func _check(condition: bool, message: String) -> void:
if not condition: if not condition:
failures.append(message) failures.append(message)
+24
View File
@@ -317,6 +317,30 @@ func _run() -> void:
), ),
"A real missing-wood fact should surface through wood-specific UI without a food cue" "A real missing-wood fact should surface through wood-specific UI without a food cue"
) )
witness.current_task = SimulationIds.ACTION_IDLE
witness.task_state = SimNPC.TASK_STATE_IDLE
witness.task_complete = true
witness.target_id = &""
witness.has_travel_target = false
witness.hunger = 20.0
witness.energy = 80.0
witness.mourning_ticks = 0
simulation_manager.simulate_tick()
var runtime_decision: ActionSelectionResult = simulation_manager.get_latest_decision(witness.id)
var helper_resource: ResourceStateRecord = simulation_manager.get_resource_state(
witness.target_id
)
_check(
(
runtime_decision != null
and runtime_decision.action_id == SimulationIds.ACTION_GATHER_WOOD
and "Responding to village need" in runtime_decision.reason
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
and helper_resource != null
and helper_resource.get_reserved_by() == witness.id
),
"The runtime helper should autonomously enter normal target resolution and reservation"
)
contributor.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0) contributor.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0)
_check( _check(
is_equal_approx( is_equal_approx(
+4 -1
View File
@@ -109,13 +109,16 @@ func _run() -> void:
) )
manager.simulate_tick() manager.simulate_tick()
var deposit_decision: ActionSelectionResult = manager.get_latest_decision(helper.id)
_check( _check(
( (
helper.current_task == SimulationIds.ACTION_DEPOSIT_FOOD helper.current_task == SimulationIds.ACTION_DEPOSIT_FOOD
and helper.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY and helper.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY
and helper.has_travel_target and helper.has_travel_target
and deposit_decision != null
and "Responding to village need" in deposit_decision.reason
), ),
"The helper should autonomously choose the real pantry deposit after gathering" "The re-derived helper should autonomously choose the real pantry deposit"
) )
helper_visual.global_position = helper.travel_target_position helper_visual.global_position = helper.travel_target_position
manager.synchronize_npc_position(helper.id, helper_visual.global_position) manager.synchronize_npc_position(helper.id, helper_visual.global_position)
+26 -2
View File
@@ -107,6 +107,26 @@ func _run() -> void:
manager.get_state_checksum() == checksum_before_helper_query, manager.get_state_checksum() == checksum_before_helper_query,
"Capable-helper discovery should not mutate authoritative simulation state" "Capable-helper discovery should not mutate authoritative simulation state"
) )
witness.current_task = SimulationIds.ACTION_IDLE
witness.task_state = SimNPC.TASK_STATE_IDLE
witness.task_complete = true
witness.target_id = &""
witness.has_travel_target = false
witness.hunger = 20.0
witness.energy = 80.0
witness.mourning_ticks = 0
manager.simulate_tick()
var helper_decision: ActionSelectionResult = manager.get_latest_decision(witness.id)
_check(
(
helper_decision != null
and helper_decision.action_id == helper_result.action_id
and "Responding to village need" in helper_decision.reason
and witness.current_task == SimulationIds.ACTION_GATHER_WOOD
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
),
"An idle derived helper should autonomously select the ordinary wood supply action"
)
var active_json: String = manager.serialize_state() var active_json: String = manager.serialize_state()
var active_restored := _create_manager(1202) var active_restored := _create_manager(1202)
@@ -127,8 +147,12 @@ func _run() -> void:
"Active restore should preserve opportunity identity and protected evidence" "Active restore should preserve opportunity identity and protected evidence"
) )
_check( _check(
restored_helper != null and restored_helper.helper_npc_id == witness.id, (
"Active restore should re-derive the same capable helper" restored_helper != null
and restored_helper.helper_npc_id == witness.id
and active_restored.npcs[witness.id].current_task == SimulationIds.ACTION_GATHER_WOOD
),
"Active restore should keep the ordinary task and re-derive the helper without assignment state"
) )
active_restored.free() active_restored.free()
+7 -1
View File
@@ -222,11 +222,17 @@ func begin_pantry_restock() -> bool:
var interested := _get_npc(_interested_villager_id) var interested := _get_npc(_interested_villager_id)
if helper == null or interested == null or simulation_manager.get_active_opportunity() == null: if helper == null or interested == null or simulation_manager.get_active_opportunity() == null:
return false return false
var opportunity_helper: OpportunityHelperResult = (
simulation_manager.get_active_opportunity_helper()
)
if opportunity_helper == null or opportunity_helper.helper_npc_id != helper.id:
return false
var selection: ActionSelectionResult = simulation_manager.action_selector.select_action( var selection: ActionSelectionResult = simulation_manager.action_selector.select_action(
helper, helper,
simulation_manager.village, simulation_manager.village,
simulation_manager.clock.time_of_day(), simulation_manager.clock.time_of_day(),
simulation_manager.npcs simulation_manager.npcs,
opportunity_helper
) )
if ( if (
selection == null selection == null