extends DialogicSubsystem ## Subsystem that manages showing and activating of choices. ## Emitted when a choice button was pressed. Info includes the keys 'button_index', 'text', 'event_index'. signal choice_selected(info:Dictionary) ## Emitted when a set of choices is reached and shown. ## Info includes the keys 'choices' (an array of dictionaries with infos on all the choices). signal question_shown(info:Dictionary) ## Contains information on the latest question. var last_question_info := {} ## The delay between the text finishing revealing and the choices appearing var reveal_delay := 0.0 ## If true the player has to click to reveal choices when they are reached var reveal_by_input := false ## The delay between the choices becoming visible and being clickable. Can prevent accidental selection. var block_delay := 0.2 ## If true, the first (top-most) choice will be focused var autofocus_first_choice := true ## If true the dialogic input action is used to trigger choices. ## However mouse events will be ignored no matter what. var use_input_action := false enum FalseBehaviour {HIDE=0, DISABLE=1} ## The behaviour of choices with a false condition and else_action set to DEFAULT. var default_false_behaviour := FalseBehaviour.HIDE enum HotkeyBehaviour {NONE, NUMBERS} ## Will add some hotkeys to the choices if different then HotkeyBehaviour.NONE. var hotkey_behaviour := HotkeyBehaviour.NONE ### INTERNALS ## Used to block choices from being clicked for a couple of seconds (if delay is set in settings). var _choice_blocker := Timer.new() #region STATE #################################################################################################### func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void: hide_all_choices() func _ready() -> void: _choice_blocker.one_shot = true DialogicUtil.update_timer_process_callback(_choice_blocker) add_child(_choice_blocker) reveal_delay = float(ProjectSettings.get_setting('dialogic/choices/reveal_delay', reveal_delay)) reveal_by_input = ProjectSettings.get_setting('dialogic/choices/reveal_by_input', reveal_by_input) block_delay = ProjectSettings.get_setting('dialogic/choices/delay', block_delay) autofocus_first_choice = ProjectSettings.get_setting('dialogic/choices/autofocus_first', autofocus_first_choice) hotkey_behaviour = ProjectSettings.get_setting('dialogic/choices/hotkey_behaviour', hotkey_behaviour) default_false_behaviour = ProjectSettings.get_setting('dialogic/choices/def_false_behaviour', default_false_behaviour) func post_install() -> void: dialogic.Inputs.dialogic_action.connect(_on_dialogic_action) #endregion #region MAIN METHODS #################################################################################################### ## Hides all choice buttons. func hide_all_choices() -> void: for node in get_tree().get_nodes_in_group('dialogic_choice_button'): node.hide() if node.is_connected('button_up', _on_choice_selected): node.disconnect('button_up', _on_choice_selected) ## Collects information on all the choices of the current question. ## The result is a dictionary like this: ## {'choices': ## [ ## {'event_index':10, 'button_index':1, 'disabled':false, 'text':"My Choice", 'visible':true}, ## {'event_index':15, 'button_index':2, 'disabled':false, 'text':"My Choice2", 'visible':true}, ## ] func get_current_question_info() -> Dictionary: var question_info := {'choices':[]} var button_idx := 1 last_question_info = {'choices':[]} for choice_index in get_current_choice_indexes(): var event: DialogicEvent = dialogic.current_timeline_events[choice_index] if not event is DialogicChoiceEvent: continue var choice_event: DialogicChoiceEvent = event var choice_info := {} choice_info['event_index'] = choice_index choice_info['button_index'] = button_idx # Check Condition var condition: String = choice_event.condition if condition.is_empty() or dialogic.Expressions.execute_condition(choice_event.condition): choice_info['disabled'] = false choice_info['text'] = choice_event.get_property_translated('text') choice_info['visible'] = true button_idx += 1 else: choice_info['disabled'] = true if not choice_event.disabled_text.is_empty(): choice_info['text'] = choice_event.get_property_translated('disabled_text') else: choice_info['text'] = choice_event.get_property_translated('text') var hide := choice_event.else_action == DialogicChoiceEvent.ElseActions.HIDE hide = hide or choice_event.else_action == DialogicChoiceEvent.ElseActions.DEFAULT and default_false_behaviour == DialogicChoiceEvent.ElseActions.HIDE choice_info['visible'] = not hide if not hide: button_idx += 1 choice_info.text = dialogic.Text.parse_text(choice_info.text, true, true, false, true, false, false) choice_info.merge(choice_event.extra_data) if dialogic.has_subsystem('History'): choice_info['visited_before'] = dialogic.History.has_event_been_visited(choice_index) question_info['choices'].append(choice_info) return question_info ## Lists all current choices and shows buttons. func show_current_question(instant:=true) -> void: hide_all_choices() _choice_blocker.stop() if !instant and (reveal_delay != 0 or reveal_by_input): if reveal_delay != 0: _choice_blocker.start(reveal_delay) _choice_blocker.timeout.connect(show_current_question) if reveal_by_input: dialogic.Inputs.dialogic_action.connect(show_current_question) return if _choice_blocker.timeout.is_connected(show_current_question): _choice_blocker.timeout.disconnect(show_current_question) if dialogic.Inputs.dialogic_action.is_connected(show_current_question): dialogic.Inputs.dialogic_action.disconnect(show_current_question) var missing_button := false var question_info := get_current_question_info() for choice in question_info.choices: var node: DialogicNode_ChoiceButton = get_choice_button_node(choice.button_index) if not node: missing_button = true continue node._load_info(choice) if choice.button_index == 1 and autofocus_first_choice: node.grab_focus() match hotkey_behaviour: ## Add 1 to 9 as shortcuts if it's enabled HotkeyBehaviour.NUMBERS: if choice.button_index > 0 or choice.button_index < 10: var shortcut: Shortcut if node.shortcut != null: shortcut = node.shortcut else: shortcut = Shortcut.new() var input_key := InputEventKey.new() input_key.keycode = OS.find_keycode_from_string(str(choice.button_index)) shortcut.events.append(input_key) node.shortcut = shortcut if node.pressed.is_connected(_on_choice_selected): node.pressed.disconnect(_on_choice_selected) node.pressed.connect(_on_choice_selected.bind(choice)) _choice_blocker.start(block_delay) question_shown.emit(question_info) if missing_button: printerr("[Dialogic] The layout you are using doesn't have enough Choice Buttons for the choices you are trying to display.") func get_choice_button_node(button_index:int) -> DialogicNode_ChoiceButton: var idx := 1 for node: DialogicNode_ChoiceButton in get_tree().get_nodes_in_group('dialogic_choice_button'): if !node.get_parent().is_visible_in_tree(): continue if node.choice_index == button_index or (node.choice_index == -1 and idx == button_index): return node if node.choice_index > 0: idx = node.choice_index idx += 1 return null func _on_choice_selected(choice_info := {}) -> void: if dialogic.paused or not _choice_blocker.is_stopped(): return if dialogic.has_subsystem('History'): var all_choices: Array = dialogic.Choices.last_question_info['choices'] if dialogic.has_subsystem('VAR'): dialogic.History.store_simple_history_entry(dialogic.VAR.parse_variables(choice_info.text), "Choice", {'all_choices': all_choices}) else: dialogic.History.store_simple_history_entry(choice_info.text, "Choice", {'all_choices': all_choices}) if dialogic.has_subsystem("History"): dialogic.History.mark_event_as_visited(choice_info.event_index) choice_selected.emit(choice_info) hide_all_choices() dialogic.current_state = dialogic.States.IDLE dialogic.handle_event(choice_info.event_index + 1) func get_current_choice_indexes() -> Array: var choices := [] var evt_idx := dialogic.current_event_idx var ignore := 0 while true: if evt_idx >= len(dialogic.current_timeline_events): break if dialogic.current_timeline_events[evt_idx] is DialogicChoiceEvent: if ignore == 0: choices.append(evt_idx) ignore += 1 elif dialogic.current_timeline_events[evt_idx].can_contain_events: ignore += 1 else: if ignore == 0: break if dialogic.current_timeline_events[evt_idx] is DialogicEndBranchEvent: ignore -= 1 evt_idx += 1 return choices func _on_dialogic_action() -> void: if get_viewport().gui_get_focus_owner() is DialogicNode_ChoiceButton and use_input_action and not dialogic.Inputs.input_was_mouse_input: get_viewport().gui_get_focus_owner().pressed.emit() #endregion #region HELPERS #################################################################################################### func is_question(index:int) -> bool: if dialogic.current_timeline_events[index] is DialogicTextEvent: if len(dialogic.current_timeline_events)-1 != index: if dialogic.current_timeline_events[index+1] is DialogicChoiceEvent: return true if dialogic.current_timeline_events[index] is DialogicChoiceEvent: if index != 0 and dialogic.current_timeline_events[index-1] is DialogicEndBranchEvent: if dialogic.current_timeline_events[dialogic.current_timeline_events[index-1].find_opening_index(index-1)] is DialogicChoiceEvent: return false else: return true else: return true return false #endregion