feat: share village needs with helpers

This commit is contained in:
Rijad Zuzo
2026-07-16 12:13:54 +02:00
parent 0332c57a89
commit d6e6a63048
17 changed files with 716 additions and 41 deletions
+187
View File
@@ -0,0 +1,187 @@
class_name VillageWhisperHud
extends Control
const DISPLAY_SECONDS := 4.2
const ENTER_OFFSET := 8.0
@export var simulation_manager: Node
@onready var accent: ColorRect = $Accent
@onready var kicker: Label = $Copy/Kicker
@onready var message: Label = $Copy/Message
var feedback_tween: Tween
var resting_position: Vector2
var feedback_version := 0
var active_trigger_event_id := OpportunityStateRecord.NO_EVENT_ID
var informed_listener_id := -1
func _ready() -> void:
resting_position = position
_reset_feedback()
if simulation_manager == null:
push_error("VillageWhisperHud: simulation_manager missing")
return
simulation_manager.opportunity_opened.connect(_on_opportunity_opened)
simulation_manager.opportunity_resolved.connect(_on_opportunity_resolved)
simulation_manager.opportunity_invalidated.connect(_on_opportunity_invalidated)
simulation_manager.event_knowledge_transferred.connect(_on_event_knowledge_transferred)
simulation_manager.npc_decision_recorded.connect(_on_npc_decision_recorded)
simulation_manager.state_restored.connect(_on_state_restored)
func _on_opportunity_opened(opportunity: OpportunityStateRecord) -> void:
active_trigger_event_id = opportunity.get_trigger_event_id()
informed_listener_id = -1
_show_feedback("Village need", _build_need_message(opportunity))
func _on_event_knowledge_transferred(
speaker_id: int, listener_id: int, event: EconomicEventRecord
) -> void:
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
if opportunity == null or int(event.data["event_id"]) != opportunity.get_trigger_event_id():
return
active_trigger_event_id = opportunity.get_trigger_event_id()
informed_listener_id = listener_id
_show_feedback(
"News travels",
(
"%s shares the %s with %s."
% [
_get_npc_name(speaker_id),
_get_need_name(opportunity),
_get_npc_name(listener_id),
]
)
)
func _on_npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult) -> void:
if npc.id != informed_listener_id:
return
var helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
if (
helper == null
or helper.trigger_event_id != active_trigger_event_id
or helper.helper_npc_id != npc.id
or helper.action_id != decision.action_id
):
return
_show_feedback(
"Help is on the way",
(
"%s heard the need and chose to %s."
% [npc.npc_name, _get_action_phrase(decision.action_id)]
)
)
func _on_opportunity_resolved(
opportunity: OpportunityStateRecord, resolution_event: EconomicEventRecord
) -> void:
if opportunity.get_trigger_event_id() != active_trigger_event_id:
return
var actor_id := int(resolution_event.data["actor_id"])
var actor_name := "The player" if actor_id < 0 else _get_npc_name(actor_id)
var outcome := (
"restocked the pantry"
if opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD
else "supplied the woodpile"
)
_show_feedback("Need met", "%s %s." % [actor_name, outcome])
informed_listener_id = -1
func _on_opportunity_invalidated(opportunity: OpportunityStateRecord) -> void:
if opportunity.get_trigger_event_id() != active_trigger_event_id:
return
_show_feedback("Need closed", "The village could no longer act on this report.")
informed_listener_id = -1
func _on_state_restored() -> void:
_reset_feedback()
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
active_trigger_event_id = (
opportunity.get_trigger_event_id()
if opportunity != null
else OpportunityStateRecord.NO_EVENT_ID
)
informed_listener_id = -1
func _build_need_message(opportunity: OpportunityStateRecord) -> String:
var interested_name := _get_npc_name(opportunity.get_interested_npc_id())
if opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD:
return "%s worries over the empty pantry." % interested_name
var trigger: EconomicEventRecord = simulation_manager.get_opportunity_trigger_event(opportunity)
var action_name := "work"
if trigger != null:
action_name = _get_action_phrase(StringName(trigger.data.get("action_id", &"")))
return "%s needs wood to finish %s." % [interested_name, action_name]
func _get_need_name(opportunity: OpportunityStateRecord) -> String:
return (
"empty-pantry worry"
if opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD
else "missing-wood report"
)
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_npc_name(npc_id: int) -> String:
for npc in simulation_manager.npcs:
if npc.id == npc_id:
return npc.npc_name
return "Someone"
func _show_feedback(heading: String, body: String) -> void:
feedback_version += 1
var version := feedback_version
if feedback_tween != null:
feedback_tween.kill()
kicker.text = heading.to_upper()
message.text = body
visible = true
position = resting_position + Vector2(0.0, ENTER_OFFSET)
modulate.a = 0.0
accent.pivot_offset = accent.size * 0.5
accent.scale.x = 0.15
feedback_tween = create_tween().set_parallel(true)
feedback_tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
feedback_tween.tween_property(self, "position", resting_position, 0.32)
feedback_tween.tween_property(self, "modulate:a", 1.0, 0.24)
feedback_tween.tween_property(accent, "scale:x", 1.0, 0.38)
_fade_after_delay(version)
func _fade_after_delay(version: int) -> void:
await get_tree().create_timer(DISPLAY_SECONDS).timeout
if version != feedback_version or not is_inside_tree():
return
feedback_tween = create_tween().set_parallel(true)
feedback_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
feedback_tween.tween_property(self, "modulate:a", 0.0, 0.45)
feedback_tween.tween_property(self, "position:y", resting_position.y - 4.0, 0.45)
await feedback_tween.finished
if version == feedback_version:
visible = false
func _reset_feedback() -> void:
feedback_version += 1
if feedback_tween != null:
feedback_tween.kill()
visible = false
position = resting_position
modulate.a = 0.0
if accent != null:
accent.scale.x = 1.0