extends SceneTree class ForgedResponse: extends RefCounted var text := "Forged authority" func get_tag_value(tag_name: String) -> String: if tag_name == "option_id": return "option_not_in_turn" if tag_name == "enabled": return "true" return "" class ForgedLine: extends RefCounted var responses: Array = [ForgedResponse.new()] var failures: Array[String] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: var main_scene: Node = load("res://main.tscn").instantiate() root.add_child(main_scene) await process_frame for _frame in 4: await physics_frame var manager: Node = main_scene.get_node("SimulationManager") var world_view: Node = main_scene.get_node("WorldViewManager") var player := main_scene.get_node("Player") as CharacterBody3D var combat := player.get_node("PlayerCombatController") as PlayerCombatController var controller := player.get_node_or_null("DialogueModeController") as DialogueModeController manager.set_process(false) player.set_physics_process(false) combat.set_physics_process(false) _freeze_and_place_population(manager, world_view, player.global_position) _check(controller != null, "The player should install one autoload-free dialogue controller") if controller == null: _finish() return var nearest_id: int = world_view.find_nearest_active_npc_id( player.global_position, player.villager_inspection_range ) var context := player.get_interaction_context() as PlayerInteractionResult _check( ( nearest_id >= 0 and context != null and context.kind == PlayerInteractionResult.KIND_DIALOGUE and context.target_id == StringName(str(nearest_id)) and "Talk" in context.prompt_text ), "The E interaction should prefer the nearest loaded living villager", ) var mouse_mode_before_dialogue := Input.mouse_mode var gamepad_accept := InputEventAction.new() gamepad_accept.action = &"ui_accept" gamepad_accept.pressed = true player.call("_unhandled_input", gamepad_accept) await _wait_for_presentation(controller) _check( ( manager.is_dialogue_active() and controller.is_dialogue_active() and player.is_dialogue_input_locked() and controller.has_valid_option_mapping() and not controller.get_presented_speaker().is_empty() and not controller.get_presented_line().is_empty() and not controller.get_presented_option_ids().is_empty() ), "A manager-planned turn should render through the pinned Dialogue Manager adapter", ) _check( player.get_interaction_context() == null, "Ordinary world interactions should be unavailable while dialogue owns input", ) var horizontal_speed_before := 5.0 player.velocity = Vector3(horizontal_speed_before, 0.0, 0.0) player.call("_physics_process", 0.1) _check( absf(player.velocity.x) < horizontal_speed_before, "Dialogue mode should decelerate the player instead of reading movement input", ) _check_combat_lock(combat) _check_navigation(controller) _check_responsive_layout(controller) var displayed_revision := controller.get_presented_revision() var authoritative_turn: ConversationTurn = manager.conversation_service.get_turn( controller.get_conversation_id() ) var forged_mapping: Array = controller.call( "_copy_mapped_responses", ForgedLine.new(), authoritative_turn ) _check( forged_mapping.is_empty(), "Presenter copy must reject response tags that are absent from the planned turn", ) var advance_option := _first_non_goodbye_option(authoritative_turn) _check(advance_option != null, "The opening turn should offer a non-terminal response") if advance_option != null: var advanced: ConversationSelectionResult = manager.conversation_service.select_option( controller.get_conversation_id(), advance_option.get_option_id(), authoritative_turn.get_revision() ) _check(advanced.was_accepted(), "The stale-revision setup should advance domain state") var stale_option_id := controller.get_presented_option_ids()[0] _check( not controller.select_option(stale_option_id), "A displayed response must not apply against a newer authoritative revision", ) await _wait_for_presentation(controller) _check( ( controller.get_presented_revision() == displayed_revision + 1 and controller.has_valid_option_mapping() ), "A stale selection should refresh from the rejected result's authoritative turn", ) _check(controller.close_conversation(), "Esc/close should end the active manager conversation") await process_frame _check( ( not manager.is_dialogue_active() and not controller.is_dialogue_active() and not player.is_dialogue_input_locked() and not controller.get_node("DialogueBalloon").visible and Input.mouse_mode == mouse_mode_before_dialogue ), "Closing should release presentation and gameplay input without one-way UI state", ) _check( controller.begin_conversation(nearest_id), "The same nearby villager should be talkable again" ) await _wait_for_presentation(controller) var goodbye_id := _option_id_for_intent( manager.conversation_service.get_turn(controller.get_conversation_id()), ConversationIntentIds.GOODBYE ) _check( not goodbye_id.is_empty() and controller.get_presented_option_ids().has(goodbye_id), "The semantic goodbye option should retain its controlled presenter mapping", ) if not goodbye_id.is_empty(): _check(controller.select_option(goodbye_id), "Selecting goodbye should be accepted") await process_frame _check( not manager.is_dialogue_active() and not controller.is_dialogue_active(), "A terminal goodbye should close through the manager's conversation-ended signal", ) main_scene.queue_free() await process_frame _finish() func _wait_for_presentation(controller: DialogueModeController) -> void: for _frame in 12: if controller.has_valid_option_mapping() or not controller.is_dialogue_active(): return await process_frame func _freeze_and_place_population(manager: Node, world_view: Node, origin: Vector3) -> void: for index in manager.npcs.size(): var npc: SimNPC = manager.npcs[index] var position := origin + Vector3(40.0 + index * 3.0, 0.0, 30.0) if index == 0: position = origin + Vector3(1.0, 0.0, 0.0) manager.synchronize_npc_position(npc.id, position) var visual := world_view.active_npc_visuals.get(npc.id) as Node3D if visual != null: visual.set_physics_process(false) if visual.has_method("stop_travel"): visual.stop_travel() visual.global_position = position func _check_combat_lock(combat: PlayerCombatController) -> void: combat.dashing = true combat.dash_velocity = Vector3(18.0, 0.0, 0.0) var attack_timer_before := combat.attack_timer combat.call("_perform_attack") _check( ( combat.get_dash_velocity() == Vector3.ZERO and is_equal_approx(combat.attack_timer, attack_timer_before) ), "Dialogue input ownership should suppress active dashes and new attacks", ) func _check_navigation(controller: DialogueModeController) -> void: var option_ids := controller.get_presented_option_ids() if option_ids.size() < 2: _check(false, "The navigation proof needs at least two presented responses") return var first_id := controller.get_focused_option_id() var down := InputEventAction.new() down.action = &"ui_down" down.pressed = true controller.call("_input", down) _check( controller.get_focused_option_id() != first_id, "Keyboard/gamepad down should move deterministic option focus", ) var up := InputEventAction.new() up.action = &"ui_up" up.pressed = true controller.call("_input", up) _check( controller.get_focused_option_id() == first_id, "Keyboard/gamepad up should restore deterministic option focus", ) func _check_responsive_layout(controller: DialogueModeController) -> void: var original_size := root.size root.size = Vector2i(480, 270) controller.call("_layout_balloon") var panel := controller.get_node("DialogueBalloon/Panel") as PanelContainer var viewport_size := controller.get_viewport().get_visible_rect().size _check( ( panel.size.x <= minf(760.0, viewport_size.x) and panel.size.y <= viewport_size.y and panel.position.x >= 0.0 and panel.position.y >= 0.0 and panel.position.x + panel.size.x <= viewport_size.x + 0.01 and panel.position.y + panel.size.y <= viewport_size.y + 0.01 ), "The custom balloon should remain inside the low-resolution viewport bounds", ) root.size = original_size func _first_non_goodbye_option(turn: ConversationTurn) -> ConversationOption: if turn == null: return null for option in turn.get_options(): if option.is_enabled() and option.get_intent_id() != ConversationIntentIds.GOODBYE: return option return null func _option_id_for_intent(turn: ConversationTurn, intent_id: StringName) -> StringName: if turn == null: return &"" for option in turn.get_options(): if option.is_enabled() and option.get_intent_id() == intent_id: return option.get_option_id() return &"" func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) func _finish() -> void: if failures.is_empty(): print("[TEST] Playable Dialogue Manager presentation passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1)