feat: surface inspected villager's open village need in field notes

This commit is contained in:
Rijad Zuzo
2026-08-10 23:36:47 +02:00
parent 8a7e6b12eb
commit 9ed985d6a3
5 changed files with 154 additions and 3 deletions
+12 -1
View File
@@ -204,7 +204,7 @@ anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 34.0
offset_top = -194.0
offset_top = -236.0
offset_right = 430.0
offset_bottom = -46.0
grow_vertical = 0
@@ -279,6 +279,17 @@ theme_override_constants/outline_size = 4
theme_override_font_sizes/font_size = 13
text = "Carrying · nothing"
[node name="Need" type="Label" parent="VillagerInspectionLayer/VillagerFieldNote/Copy"]
custom_minimum_size = Vector2(380, 0)
layout_mode = 2
mouse_filter = 2
theme_override_colors/font_color = Color(0.98, 0.78, 0.36, 1)
theme_override_colors/font_outline_color = Color(0.07, 0.045, 0.025, 0.9)
theme_override_constants/outline_size = 4
theme_override_font_sizes/font_size = 13
text = "Need"
autowrap_mode = 2
[node name="Reason" type="Label" parent="VillagerInspectionLayer/VillagerFieldNote/Copy"]
custom_minimum_size = Vector2(380, 0)
layout_mode = 2
+15 -1
View File
@@ -14,6 +14,11 @@ var carried_amounts: Dictionary
var decision_reason: String
var _has_decision_reason: bool
var has_open_need := false
var need_type: StringName
var need_progress: String
var need_response: String
func _init(
inspection_npc_id: int,
@@ -24,7 +29,8 @@ func _init(
inspection_target_id: StringName,
inspection_target_name: String,
inspection_carried_amounts: Dictionary,
inspection_decision_reason := ""
inspection_decision_reason := "",
inspection_need: Dictionary = {}
) -> void:
npc_id = inspection_npc_id
npc_name = inspection_npc_name
@@ -36,6 +42,10 @@ func _init(
carried_amounts = inspection_carried_amounts.duplicate(true)
_has_decision_reason = not inspection_decision_reason.strip_edges().is_empty()
decision_reason = (inspection_decision_reason if _has_decision_reason else AWAITING_REASON)
has_open_need = bool(inspection_need.get("has_open_need", false))
need_type = StringName(inspection_need.get("need_type", &""))
need_progress = String(inspection_need.get("progress", ""))
need_response = String(inspection_need.get("response", ""))
func has_decision_reason() -> bool:
@@ -80,6 +90,10 @@ func cache_key() -> String:
target_name,
",".join(carried_parts),
decision_reason,
str(has_open_need),
String(need_type),
need_progress,
need_response,
]
)
)
+97 -1
View File
@@ -142,6 +142,7 @@ func get_nearby_villager_inspection() -> VillagerInspectionResult:
var decision: ActionSelectionResult = simulation_manager.get_latest_decision(npc.id)
if decision != null and decision.action_id == npc.current_task:
reason = decision.reason
var need := _get_open_need_for_npc(npc)
return VillagerInspectionResult.new(
npc.id,
npc.npc_name,
@@ -151,10 +152,105 @@ func get_nearby_villager_inspection() -> VillagerInspectionResult:
npc.target_id,
target_name,
carried_amounts,
reason
reason,
need
)
func _get_open_need_for_npc(npc: SimNPC) -> Dictionary:
if not simulation_manager.has_method("get_latest_opportunity_for_npc"):
return {
"has_open_need": false,
"need_type": &"",
"progress": "",
"response": "",
}
var opportunity: OpportunityStateRecord = simulation_manager.get_latest_opportunity_for_npc(
npc.id
)
if (
opportunity == null
or opportunity.get_status() != OpportunityStateRecord.STATUS_OPEN
or opportunity.get_interested_npc_id() != npc.id
):
return {
"has_open_need": false,
"need_type": &"",
"progress": "",
"response": "",
}
var resource_name := String(opportunity.get_resource_id()).capitalize()
var current_amount := _get_need_storage_amount(opportunity)
var progress := (
"%s %.0f / %.0f" % [resource_name, current_amount, opportunity.get_target_amount()]
)
var response := "No one is able to supply this right now."
if simulation_manager.has_method("get_active_opportunity_helper"):
var helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
if helper != null and helper.opportunity_id == opportunity.get_opportunity_id():
var helper_name := _get_npc_name(helper.helper_npc_id)
var action_phrase := _get_action_phrase(helper.action_id)
response = "%s can %s." % [helper_name, action_phrase]
if response == "No one is able to supply this right now.":
if simulation_manager.has_method("get_active_opportunity_player_response"):
var player_route: OpportunityPlayerResponseResult = (
simulation_manager.get_active_opportunity_player_response()
)
if player_route != null:
var route_phrase := (
"forage food"
if opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD
else "harvest a tree"
)
response = (
"You can help — %s to the %s."
% [route_phrase, _get_need_target_name(opportunity)]
)
return {
"has_open_need": true,
"need_type": opportunity.get_opportunity_type(),
"progress": progress,
"response": response,
}
func _get_need_storage_amount(opportunity: OpportunityStateRecord) -> float:
if not simulation_manager.has_method("get_pantry"):
return 0.0
match opportunity.get_target_id():
SimulationIds.STORAGE_VILLAGE_PANTRY:
var pantry: StorageStateRecord = simulation_manager.get_pantry()
return pantry.get_amount(opportunity.get_resource_id()) if pantry != null else 0.0
SimulationIds.STORAGE_VILLAGE_WOODPILE:
if simulation_manager.has_method("get_woodpile"):
var woodpile: StorageStateRecord = simulation_manager.get_woodpile()
return (
woodpile.get_amount(opportunity.get_resource_id()) if woodpile != null else 0.0
)
return 0.0
func _get_npc_name(npc_id: int) -> String:
for npc in simulation_manager.npcs:
if npc.id == npc_id:
return npc.npc_name
return "Someone"
func _get_action_phrase(action_id: StringName) -> String:
var definition := SimulationDefinitions.get_action(action_id)
return definition.display_name.to_lower() if definition != null else String(action_id)
func _get_need_target_name(opportunity: OpportunityStateRecord) -> String:
match opportunity.get_target_id():
SimulationIds.STORAGE_VILLAGE_PANTRY:
return "village pantry"
SimulationIds.STORAGE_VILLAGE_WOODPILE:
return "village woodpile"
return String(opportunity.get_target_id()).replace("_", " ")
func _find_feed_animal() -> AnimalNode:
if not ("animal_care" in simulation_manager) or simulation_manager.animal_care == null:
push_error("Player: SimulationManager has no animal-care service")
+24
View File
@@ -214,6 +214,30 @@ func _run() -> void:
),
"The interested villager should expose the open pantry need in their inspector"
)
var field_note := (
main_scene.get_node("VillagerInspectionLayer/VillagerFieldNote") as VillagerFieldNoteHud
)
contributor_visual.global_position = Vector3(-30.0, 0.0, -30.0)
var note_player := main_scene.get_node("Player") as Node3D
note_player.global_position = contributor_visual.global_position + Vector3(1.0, 0.0, 0.0)
var note_context: VillagerInspectionResult = note_player.get_nearby_villager_inspection()
_check(
(
note_context != null
and note_context.npc_id == contributor.id
and note_context.has_open_need
and note_context.need_type == SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY
and note_context.need_progress == "Food 0 / 1"
and not note_context.need_response.strip_edges().is_empty()
),
"Field note should derive the inspected villager's exact open need and response"
)
field_note.refresh_note(true)
var need_label := field_note.get_node("Copy/Need") as Label
_check(
field_note.visible and need_label.visible and "Food 0 / 1" in need_label.text,
"Field note HUD should render the surfaced need line for its interested villager"
)
village_ui.selected_npc_index = listener.id
village_ui.call("_refresh_npc_inspector")
_check(
+6
View File
@@ -13,6 +13,7 @@ const EXIT_OFFSET := 4.0
@onready var activity_label: Label = $Copy/Activity
@onready var target_label: Label = $Copy/Target
@onready var carrying_label: Label = $Copy/Carrying
@onready var need_label: Label = $Copy/Need
@onready var reason_label: Label = $Copy/Reason
var copy_resting_position: Vector2
@@ -63,6 +64,11 @@ func _present(context: VillagerInspectionResult, animate: bool) -> void:
activity_label.text = context.activity_text()
target_label.text = "Target · %s" % context.target_name
carrying_label.text = "Carrying · %s" % context.carrying_text()
if context.has_open_need:
need_label.text = "Need · %s\n%s" % [context.need_progress, context.need_response]
need_label.visible = true
else:
need_label.visible = false
reason_label.text = "Why · %s" % context.decision_reason
visible = true
accent.pivot_offset = accent.size * 0.5