class_name VillageWhisperHud extends Control const DISPLAY_SECONDS := 4.2 const ENTER_OFFSET := 8.0 const FEEDBACK_PLAYER_RESPONSE := &"player_response" @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 var feedback_kind := &"" var player_response_available := false 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.event_knowledge_changed.connect(_on_helper_facts_changed) simulation_manager.event_knowledge_forgotten.connect(_on_helper_fact_forgotten) simulation_manager.relationship_changed.connect(_on_helper_relationship_changed) simulation_manager.npc_inventory_changed.connect(_on_helper_inventory_changed) simulation_manager.village_changed.connect(_on_village_changed) simulation_manager.state_restored.connect(_on_state_restored) player_response_available = simulation_manager.get_active_opportunity_player_response() != null func _on_opportunity_opened(opportunity: OpportunityStateRecord) -> void: active_trigger_event_id = opportunity.get_trigger_event_id() informed_listener_id = -1 var player_response: OpportunityPlayerResponseResult = ( simulation_manager.get_active_opportunity_player_response() ) player_response_available = player_response != null if player_response != null: _show_feedback( "You can help", _build_player_response_message(player_response), FEEDBACK_PLAYER_RESPONSE ) return _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_helper_facts_changed(_knower_id: int, _event: EconomicEventRecord) -> void: _refresh_player_response() func _on_helper_fact_forgotten(_knower_id: int, _event: EconomicEventRecord) -> void: _refresh_player_response() func _on_helper_relationship_changed( _relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord ) -> void: _refresh_player_response() func _on_helper_inventory_changed(_npc: SimNPC, _item_id: StringName, _amount: float) -> void: _refresh_player_response() func _on_village_changed(_village: SimVillage) -> void: _refresh_player_response() func _refresh_player_response() -> void: var response: OpportunityPlayerResponseResult = ( simulation_manager.get_active_opportunity_player_response() ) var is_available := response != null if is_available == player_response_available: return player_response_available = is_available if response != null: _show_feedback( "You can help", _build_player_response_message(response), FEEDBACK_PLAYER_RESPONSE ) elif feedback_kind == FEEDBACK_PLAYER_RESPONSE: _reset_feedback() 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 player_response_available = false 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 player_response_available = false 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 player_response_available = simulation_manager.get_active_opportunity_player_response() != null 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 _build_player_response_message(response: OpportunityPlayerResponseResult) -> String: var destination := _get_destination_name(response.target_id) if response.resource_id == SimulationIds.RESOURCE_FOOD: return "Forage food; it goes straight to the %s." % destination return "Harvest a tree; wood goes straight to the %s." % destination func _get_destination_name(target_id: StringName) -> String: match target_id: SimulationIds.STORAGE_VILLAGE_PANTRY: return "village pantry" SimulationIds.STORAGE_VILLAGE_WOODPILE: return "village woodpile" return String(target_id).replace("_", " ") 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, kind: StringName = &"") -> void: feedback_version += 1 var version := feedback_version if feedback_tween != null: feedback_tween.kill() kicker.text = heading.to_upper() message.text = body feedback_kind = kind 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 feedback_kind = &"" func _reset_feedback() -> void: feedback_version += 1 if feedback_tween != null: feedback_tween.kill() visible = false feedback_kind = &"" position = resting_position modulate.a = 0.0 if accent != null: accent.scale.x = 1.0