feat: surface player routes for village needs
This commit is contained in:
@@ -311,16 +311,25 @@ func _run() -> void:
|
||||
runtime_opportunity.get_trigger_event_id() if runtime_opportunity != null else -1
|
||||
)
|
||||
var runtime_helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
||||
var runtime_player_response: OpportunityPlayerResponseResult = (
|
||||
simulation_manager.get_active_opportunity_player_response()
|
||||
)
|
||||
_check(
|
||||
(
|
||||
runtime_opportunity != null
|
||||
and runtime_helper == null
|
||||
and runtime_player_response != null
|
||||
and runtime_player_response.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||
and runtime_player_response.target_id == SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||
and runtime_player_response.available_source_count > 0
|
||||
and not simulation_manager.npc_knows_event(witness.id, runtime_trigger_id)
|
||||
and whisper_kicker.text == "VILLAGE NEED"
|
||||
and contributor.npc_name in whisper_message.text
|
||||
and "needs wood" in whisper_message.text
|
||||
and whisper_kicker.text == "YOU CAN HELP"
|
||||
and (
|
||||
"Harvest a tree; wood goes straight to the village woodpile."
|
||||
in whisper_message.text
|
||||
)
|
||||
),
|
||||
"The real blocked-work need should surface while the distant helper remains uninformed"
|
||||
"An unassisted blocked-work need should surface its real player harvest route"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
@@ -330,6 +339,7 @@ func _run() -> void:
|
||||
"Possible helper: none informed, trusted, and able to supply"
|
||||
in village_stats_label.text
|
||||
)
|
||||
and "Player route: Gather Wood → Village Woodpile" in village_stats_label.text
|
||||
and "◆ Find wood for Study" in inspector_label.text
|
||||
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
||||
),
|
||||
@@ -348,6 +358,7 @@ func _run() -> void:
|
||||
witness.id, runtime_trigger_id
|
||||
)
|
||||
runtime_helper = simulation_manager.get_active_opportunity_helper()
|
||||
runtime_player_response = simulation_manager.get_active_opportunity_player_response()
|
||||
_check(
|
||||
(
|
||||
communicated_trigger != null
|
||||
@@ -357,6 +368,7 @@ func _run() -> void:
|
||||
)
|
||||
and communicated_trigger.get_source_npc_id() == contributor.id
|
||||
and runtime_helper != null
|
||||
and runtime_player_response == null
|
||||
and runtime_helper.helper_npc_id == witness.id
|
||||
and runtime_helper.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||
and whisper_kicker.text == "NEWS TRAVELS"
|
||||
|
||||
@@ -306,6 +306,60 @@ func test_ready_inventory_then_trust_and_stable_id_rank_helpers_deterministicall
|
||||
assert_eq(result.helper_npc_id, lower_id_carrier.id)
|
||||
|
||||
|
||||
func test_player_response_requires_no_helper_and_a_real_player_supply_route() -> void:
|
||||
var fixture := _open_fixture()
|
||||
var system: VillageOpportunitySystem = fixture["system"]
|
||||
var pantry: StorageStateRecord = fixture["pantry"]
|
||||
var opportunity: OpportunityStateRecord = system.get_open_opportunity()
|
||||
var player_source := _resource_state(
|
||||
&"player_berries", SimulationIds.ACTION_GATHER_FOOD, SimulationIds.RESOURCE_FOOD, 1.0
|
||||
)
|
||||
var npc_only_source := _resource_state(
|
||||
&"npc_berries",
|
||||
SimulationIds.ACTION_GATHER_FOOD,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
2.0,
|
||||
0.0,
|
||||
0.0,
|
||||
false
|
||||
)
|
||||
var resource_states := {
|
||||
player_source.get_node_id(): player_source,
|
||||
npc_only_source.get_node_id(): npc_only_source,
|
||||
}
|
||||
assert_true(player_source.reserve(99))
|
||||
var opportunity_before := opportunity.to_dictionary()
|
||||
var pantry_before := pantry.to_dictionary()
|
||||
var source_before := player_source.to_dictionary()
|
||||
var response: OpportunityPlayerResponseResult = system.find_player_response(
|
||||
opportunity, pantry, resource_states, false
|
||||
)
|
||||
|
||||
assert_not_null(response)
|
||||
assert_eq(response.opportunity_id, opportunity.get_opportunity_id())
|
||||
assert_eq(response.trigger_event_id, opportunity.get_trigger_event_id())
|
||||
assert_eq(response.action_id, SimulationIds.ACTION_GATHER_FOOD)
|
||||
assert_eq(response.resource_id, SimulationIds.RESOURCE_FOOD)
|
||||
assert_eq(response.target_id, SimulationIds.STORAGE_VILLAGE_PANTRY)
|
||||
assert_eq(response.available_source_count, 1)
|
||||
assert_string_contains(response.reason, "No capable helper")
|
||||
assert_string_contains(response.reason, "player-usable finite Food source")
|
||||
assert_eq(opportunity.to_dictionary(), opportunity_before)
|
||||
assert_eq(pantry.to_dictionary(), pantry_before)
|
||||
assert_eq(player_source.to_dictionary(), source_before)
|
||||
assert_null(system.find_player_response(opportunity, pantry, resource_states, true))
|
||||
|
||||
player_source.set_enabled(false)
|
||||
assert_null(system.find_player_response(opportunity, pantry, resource_states, false))
|
||||
var full_pantry := StorageStateRecord.create(
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY, {String(SimulationIds.RESOURCE_WOOD): 1.0}, 1.0
|
||||
)
|
||||
player_source.set_enabled(true)
|
||||
assert_null(system.find_player_response(opportunity, full_pantry, resource_states, false))
|
||||
var wrong_target := StorageStateRecord.create(&"not_the_pantry", {}, 1.0)
|
||||
assert_null(system.find_player_response(opportunity, wrong_target, resource_states, false))
|
||||
|
||||
|
||||
func test_wood_helper_uses_available_finite_source_and_respects_reservations() -> void:
|
||||
var system := VillageOpportunitySystemScript.new()
|
||||
var interested := _npc(0, 20.0)
|
||||
@@ -483,7 +537,9 @@ func _resource_state(
|
||||
resource_id: StringName,
|
||||
amount: float,
|
||||
discovery_priority: float = 0.0,
|
||||
safety_risk: float = 0.0
|
||||
safety_risk: float = 0.0,
|
||||
can_player_use: bool = true,
|
||||
can_npcs_use: bool = true
|
||||
) -> ResourceStateRecord:
|
||||
var node := ResourceNode.new()
|
||||
node.node_id = node_id
|
||||
@@ -493,6 +549,8 @@ func _resource_state(
|
||||
node.yield_per_action = 1.0
|
||||
node.discovery_priority = discovery_priority
|
||||
node.safety_risk = safety_risk
|
||||
node.can_player_use = can_player_use
|
||||
node.can_npcs_use = can_npcs_use
|
||||
var state := ResourceStateRecord.create_from_node(node)
|
||||
node.free()
|
||||
return state
|
||||
|
||||
@@ -202,6 +202,48 @@ func _run() -> void:
|
||||
root.add_child(tree)
|
||||
await process_frame
|
||||
_check(player_manager.register_resource_node(tree), "Player branch should bind a real tree")
|
||||
var checksum_before_player_query: String = player_manager.get_state_checksum()
|
||||
var player_response: OpportunityPlayerResponseResult = (
|
||||
player_manager.get_active_opportunity_player_response()
|
||||
)
|
||||
_check(
|
||||
(
|
||||
player_manager.get_active_opportunity_helper() == null
|
||||
and player_response != null
|
||||
and player_response.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||
and player_response.resource_id == SimulationIds.RESOURCE_WOOD
|
||||
and player_response.target_id == SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||
and player_response.available_source_count == 1
|
||||
),
|
||||
"An unassisted wood need should derive one ordinary player tree-to-woodpile route"
|
||||
)
|
||||
_check(
|
||||
player_manager.get_state_checksum() == checksum_before_player_query,
|
||||
"Player-response discovery should not mutate authoritative simulation state"
|
||||
)
|
||||
var player_active_json: String = player_manager.serialize_state()
|
||||
var player_active_restored := _create_manager(1210)
|
||||
_check(
|
||||
player_active_restored.restore_state_from_json(player_active_json),
|
||||
"An active player-response route should restore through its existing facts"
|
||||
)
|
||||
var restored_player_response: OpportunityPlayerResponseResult = (
|
||||
player_active_restored.get_active_opportunity_player_response()
|
||||
)
|
||||
_check(
|
||||
(
|
||||
player_active_restored.get_state_checksum() == checksum_before_player_query
|
||||
and restored_player_response != null
|
||||
and restored_player_response.action_id == player_response.action_id
|
||||
and restored_player_response.target_id == player_response.target_id
|
||||
and (
|
||||
restored_player_response.available_source_count
|
||||
== player_response.available_source_count
|
||||
)
|
||||
),
|
||||
"Restore should re-derive the same player response without persisted hint state"
|
||||
)
|
||||
player_active_restored.free()
|
||||
_check(
|
||||
is_equal_approx(player_manager.harvest_resource_node(tree), 1.0),
|
||||
"Player tree extraction should supply the authoritative woodpile"
|
||||
@@ -227,6 +269,10 @@ func _run() -> void:
|
||||
),
|
||||
"Player extraction should resolve through its exact existing economy event"
|
||||
)
|
||||
_check(
|
||||
player_manager.get_active_opportunity_player_response() == null,
|
||||
"Resolving the need should remove the derived player response"
|
||||
)
|
||||
tree.queue_free()
|
||||
player_manager.free()
|
||||
|
||||
@@ -347,6 +393,7 @@ func _create_player_tree() -> ResourceNode:
|
||||
tree.resource_id = SimulationIds.RESOURCE_WOOD
|
||||
tree.initial_amount = 1.0
|
||||
tree.yield_per_action = 1.0
|
||||
tree.can_npcs_use = false
|
||||
tree.can_player_use = true
|
||||
tree.debug_label_enabled = false
|
||||
var interaction_point := Marker3D.new()
|
||||
@@ -359,6 +406,7 @@ func _create_helper_tree() -> ResourceNode:
|
||||
var tree := _create_player_tree()
|
||||
tree.name = "HelperTree"
|
||||
tree.node_id = &"helper_tree"
|
||||
tree.can_npcs_use = true
|
||||
tree.can_player_use = false
|
||||
tree.discovery_priority = 4.0
|
||||
return tree
|
||||
|
||||
Reference in New Issue
Block a user