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