feat: surface open opportunity need on villager field note
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||
root.add_child(main_scene)
|
||||
await process_frame
|
||||
for _frame in 10:
|
||||
await physics_frame
|
||||
|
||||
var manager: Node = main_scene.get_node("SimulationManager")
|
||||
var view: Node = main_scene.get_node("WorldViewManager")
|
||||
var player: CharacterBody3D = main_scene.get_node("Player")
|
||||
var demo: Node = main_scene.get_node("DemoController")
|
||||
var field_note := main_scene.get_node("VillagerInspectionLayer/VillagerFieldNote")
|
||||
manager.set_process(false)
|
||||
player.set_physics_process(false)
|
||||
_freeze_visuals(view)
|
||||
|
||||
var staged: Dictionary = demo.call("stage_pantry_crisis")
|
||||
_check(not staged.is_empty(), "The runtime proof should stage a real open pantry need")
|
||||
if staged.is_empty():
|
||||
_finish()
|
||||
return
|
||||
var interested: SimNPC = staged["interested"]
|
||||
var pantry_position: Vector3 = (
|
||||
StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY).get_interaction_position()
|
||||
)
|
||||
player.global_position = pantry_position
|
||||
_move_population_away(manager, view, interested.id, pantry_position)
|
||||
_set_loaded_position(manager, view, interested, pantry_position + Vector3(1.0, 0.0, 0.0))
|
||||
|
||||
var checksum_before: String = manager.get_state_checksum()
|
||||
var serialized_before: String = manager.serialize_state()
|
||||
var events_before: int = manager.economic_events.size()
|
||||
for _query in 3:
|
||||
player.call("get_nearby_villager_inspection")
|
||||
field_note.call("refresh_note", true)
|
||||
var need_label := field_note.get_node("Copy/Need") as Label
|
||||
_check(
|
||||
(
|
||||
manager.get_state_checksum() == checksum_before
|
||||
and manager.serialize_state() == serialized_before
|
||||
and manager.economic_events.size() == events_before
|
||||
and field_note.visible
|
||||
and need_label.visible
|
||||
and "Need" in need_label.text
|
||||
),
|
||||
"The inspected interested villager should surface the real need without mutating state",
|
||||
)
|
||||
if not need_label.visible:
|
||||
_finish()
|
||||
return
|
||||
|
||||
var helper: OpportunityHelperResult = manager.get_active_opportunity_helper()
|
||||
var helper_npc := _find_npc(manager.npcs, helper.helper_npc_id) if helper != null else null
|
||||
_check(
|
||||
(
|
||||
helper != null
|
||||
and helper_npc != null
|
||||
and helper_npc.npc_name in need_label.text
|
||||
and "bringing" in need_label.text.to_lower()
|
||||
),
|
||||
"The open need with a capable helper should name that helper on the field note",
|
||||
)
|
||||
|
||||
var checksum_after_helper: String = manager.get_state_checksum()
|
||||
var food_node: ResourceNode = _find_food_node(manager)
|
||||
_check(food_node != null, "The resolution proof needs a stocked player-usable food source")
|
||||
if food_node == null:
|
||||
_finish()
|
||||
return
|
||||
var harvested: float = manager.harvest_resource_node(food_node)
|
||||
_check(
|
||||
(
|
||||
harvested > 0.0
|
||||
and manager.get_active_opportunity() == null
|
||||
and (
|
||||
manager.opportunity_system.get_latest().get_status()
|
||||
== OpportunityStateRecord.STATUS_RESOLVED
|
||||
)
|
||||
),
|
||||
"An ordinary finite-resource harvest should resolve the same open need",
|
||||
)
|
||||
field_note.call("refresh_note", true)
|
||||
_check(
|
||||
not need_label.visible and manager.get_state_checksum() != checksum_after_helper,
|
||||
"Resolving the need should clear the surfaced need segment from the field note",
|
||||
)
|
||||
|
||||
_finish()
|
||||
|
||||
|
||||
func _find_food_node(manager: Node) -> ResourceNode:
|
||||
for node in ResourceNode.get_all():
|
||||
var state: ResourceStateRecord = (
|
||||
manager.get_resource_state(node.node_id) as ResourceStateRecord
|
||||
)
|
||||
if (
|
||||
node.resource_id == SimulationIds.RESOURCE_FOOD
|
||||
and state != null
|
||||
and state.can_player_use_resource()
|
||||
and state.can_extract()
|
||||
and state.get_amount_remaining() >= 1.0
|
||||
):
|
||||
return node
|
||||
return null
|
||||
|
||||
|
||||
func _find_npc(npcs: Array[SimNPC], npc_id: int) -> SimNPC:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
return npc
|
||||
return null
|
||||
|
||||
|
||||
func _move_population_away(manager: Node, view: Node, keep_id: int, origin: Vector3) -> void:
|
||||
for npc in manager.npcs:
|
||||
if npc.id == keep_id:
|
||||
continue
|
||||
_set_loaded_position(
|
||||
manager, view, npc, origin + Vector3(40.0 + float(npc.id) * 2.0, 0.0, 20.0)
|
||||
)
|
||||
|
||||
|
||||
func _set_loaded_position(manager: Node, view: Node, npc: SimNPC, position: Vector3) -> void:
|
||||
manager.synchronize_npc_position(npc.id, position)
|
||||
var visual := view.active_npc_visuals.get(npc.id) as Node3D
|
||||
if visual != null:
|
||||
visual.set_physics_process(false)
|
||||
if visual.has_method("stop_travel"):
|
||||
visual.stop_travel()
|
||||
visual.global_position = position
|
||||
|
||||
|
||||
func _freeze_visuals(view: Node) -> void:
|
||||
for visual in view.active_npc_visuals.values():
|
||||
visual.set_physics_process(false)
|
||||
if visual.has_method("stop_travel"):
|
||||
visual.stop_travel()
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Field-note opportunity surfacing passed")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dg66jcyhajmxp
|
||||
@@ -0,0 +1,168 @@
|
||||
extends GutTest
|
||||
|
||||
const VillagerOpportunityNoteScript := preload("res://player/VillagerOpportunityNote.gd")
|
||||
|
||||
|
||||
func test_no_opportunity_produces_no_need() -> void:
|
||||
var note := VillagerOpportunityNote.derive(null, null, null, "", null, "", "")
|
||||
|
||||
assert_not_null(note)
|
||||
assert_false(note.has_need)
|
||||
assert_eq(note.help_kind, &"")
|
||||
|
||||
|
||||
func test_player_route_wins_over_helper_when_derivable() -> void:
|
||||
var opportunity := _open_pantry_opportunity()
|
||||
var response := _player_response()
|
||||
var helper := _helper(7)
|
||||
var pantry := _pantry(0.0, 100.0)
|
||||
|
||||
var note := VillagerOpportunityNote.derive(
|
||||
opportunity, response, helper, "Tarik", pantry, "Village Pantry", "food"
|
||||
)
|
||||
|
||||
assert_true(note.has_need)
|
||||
assert_eq(note.help_kind, VillagerOpportunityNote.HELP_PLAYER_ROUTE)
|
||||
assert_eq(note.need_text, "The Village Pantry needs food")
|
||||
assert_true(note.help_text.contains("You can help"))
|
||||
assert_true(note.help_text.contains("Village Pantry"))
|
||||
|
||||
|
||||
func test_helper_is_named_when_no_player_route() -> void:
|
||||
var opportunity := _open_pantry_opportunity()
|
||||
var pantry := _pantry(0.0, 100.0)
|
||||
|
||||
var note := VillagerOpportunityNote.derive(
|
||||
opportunity, null, _helper(7), "Tarik", pantry, "Village Pantry", "food"
|
||||
)
|
||||
|
||||
assert_true(note.has_need)
|
||||
assert_eq(note.help_kind, VillagerOpportunityNote.HELP_HELPER)
|
||||
assert_true(note.help_text.contains("Tarik"))
|
||||
assert_true(note.help_text.contains("food"))
|
||||
|
||||
|
||||
func test_unavailable_reports_no_room_when_storage_is_full() -> void:
|
||||
var opportunity := _open_pantry_opportunity()
|
||||
var pantry := _pantry(0.0, 0.5)
|
||||
|
||||
var note := VillagerOpportunityNote.derive(
|
||||
opportunity, null, null, "", pantry, "Village Pantry", "food"
|
||||
)
|
||||
|
||||
assert_true(note.has_need)
|
||||
assert_eq(note.help_kind, VillagerOpportunityNote.HELP_UNAVAILABLE)
|
||||
assert_true(note.help_text.contains("no room"))
|
||||
|
||||
|
||||
func test_unavailable_reports_need_already_met() -> void:
|
||||
var opportunity := _open_pantry_opportunity()
|
||||
var pantry := _pantry(1.0, 100.0)
|
||||
|
||||
var note := VillagerOpportunityNote.derive(
|
||||
opportunity, null, null, "", pantry, "Village Pantry", "food"
|
||||
)
|
||||
|
||||
assert_true(note.has_need)
|
||||
assert_eq(note.help_kind, VillagerOpportunityNote.HELP_UNAVAILABLE)
|
||||
assert_true(note.help_text.contains("already has enough"))
|
||||
|
||||
|
||||
func test_unavailable_reports_no_stocked_source_when_storage_has_room() -> void:
|
||||
var opportunity := _open_pantry_opportunity()
|
||||
var pantry := _pantry(0.0, 100.0)
|
||||
|
||||
var note := VillagerOpportunityNote.derive(
|
||||
opportunity, null, null, "", pantry, "Village Pantry", "food"
|
||||
)
|
||||
|
||||
assert_true(note.has_need)
|
||||
assert_eq(note.help_kind, VillagerOpportunityNote.HELP_UNAVAILABLE)
|
||||
assert_true(note.help_text.contains("No stocked source"))
|
||||
|
||||
|
||||
func test_unavailable_reports_missing_storage() -> void:
|
||||
var opportunity := _open_pantry_opportunity()
|
||||
|
||||
var note := VillagerOpportunityNote.derive(
|
||||
opportunity, null, null, "", null, "Village Pantry", "food"
|
||||
)
|
||||
|
||||
assert_true(note.has_need)
|
||||
assert_eq(note.help_kind, VillagerOpportunityNote.HELP_UNAVAILABLE)
|
||||
assert_true(note.help_text.contains("No storage"))
|
||||
|
||||
|
||||
func test_cache_key_uses_need_and_help_state() -> void:
|
||||
var opportunity := _open_pantry_opportunity()
|
||||
var pantry := _pantry(0.0, 100.0)
|
||||
var helper_note := VillagerOpportunityNote.derive(
|
||||
opportunity, null, _helper(7), "Tarik", pantry, "Village Pantry", "food"
|
||||
)
|
||||
var unavailable_note := VillagerOpportunityNote.derive(
|
||||
opportunity, null, null, "", pantry, "Village Pantry", "food"
|
||||
)
|
||||
var no_need := VillagerOpportunityNote.derive(null, null, null, "", null, "", "")
|
||||
|
||||
assert_ne(helper_note.cache_key(), unavailable_note.cache_key())
|
||||
assert_ne(no_need.cache_key(), helper_note.cache_key())
|
||||
|
||||
|
||||
func _open_pantry_opportunity() -> OpportunityStateRecord:
|
||||
return OpportunityStateRecord.create(
|
||||
0,
|
||||
10,
|
||||
5,
|
||||
3,
|
||||
1.0,
|
||||
SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY,
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD
|
||||
)
|
||||
|
||||
|
||||
func _player_response() -> OpportunityPlayerResponseResult:
|
||||
return (
|
||||
OpportunityPlayerResponseResult
|
||||
. new(
|
||||
{
|
||||
"opportunity_id": 0,
|
||||
"trigger_event_id": 5,
|
||||
"action_id": SimulationIds.ACTION_GATHER_FOOD,
|
||||
"resource_id": SimulationIds.RESOURCE_FOOD,
|
||||
"target_id": SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
"available_source_count": 2,
|
||||
"reason":
|
||||
"No capable helper; 2 player-usable finite Food sources can supply village_pantry",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _helper(helper_id: int) -> OpportunityHelperResult:
|
||||
return (
|
||||
OpportunityHelperResult
|
||||
. new(
|
||||
{
|
||||
"opportunity_id": 0,
|
||||
"helper_npc_id": helper_id,
|
||||
"action_id": SimulationIds.ACTION_GATHER_FOOD,
|
||||
"source_id": "",
|
||||
"resource_id": SimulationIds.RESOURCE_FOOD,
|
||||
"trigger_event_id": 5,
|
||||
"trust": 0.8,
|
||||
"familiarity": 0.5,
|
||||
"uses_inventory": false,
|
||||
"available_source_count": 1,
|
||||
"profession_match": true,
|
||||
"reason":
|
||||
"Knows the need; trust 0.80 toward Amina; has 1 available finite Food source",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _pantry(amount: float, capacity: float) -> StorageStateRecord:
|
||||
return StorageStateRecord.create(
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY, {SimulationIds.RESOURCE_FOOD: amount}, capacity
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://1chfro4iw1o5
|
||||
Reference in New Issue
Block a user