From 41b3092552a13ff24e2daf7bf969a78e74fa2489 Mon Sep 17 00:00:00 2001 From: hz Date: Fri, 7 Aug 2026 17:56:49 -0500 Subject: [PATCH] Polish fullscreen CCS dialogue --- scenes/ccs_cases_2/doctor_office.tscn | 7 ++++--- scripts/ui/ccs_cases_2/doctor_office.gd | 11 +++++++--- scripts/ui/ccs_cases_2_screen.gd | 5 +++++ scripts/ui/main.gd | 28 +++++++++++++++++++------ tests/ccs_cases_2_ui_test.gd | 18 ++++++++++++++++ 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/scenes/ccs_cases_2/doctor_office.tscn b/scenes/ccs_cases_2/doctor_office.tscn index 1174050..a6988d1 100644 --- a/scenes/ccs_cases_2/doctor_office.tscn +++ b/scenes/ccs_cases_2/doctor_office.tscn @@ -136,7 +136,7 @@ anchor_bottom = 1.0 offset_left = 96.0 offset_top = 31.0 offset_right = -64.0 -offset_bottom = -52.0 +offset_bottom = -64.0 grow_horizontal = 2 grow_vertical = 2 theme_override_colors/font_color = Color(0.055, 0.105, 0.145, 1) @@ -145,6 +145,7 @@ text = "Hello, doctor." fit_content = false scroll_active = false autowrap_mode = 2 +visible_characters_behavior = 1 vertical_alignment = 1 [node name="ContinueHint" type="Label" parent="DialogueBubble"] @@ -155,9 +156,9 @@ anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 offset_left = -330.0 -offset_top = -60.0 +offset_top = -55.0 offset_right = -90.0 -offset_bottom = -32.0 +offset_bottom = -27.0 grow_horizontal = 0 grow_vertical = 0 mouse_filter = 2 diff --git a/scripts/ui/ccs_cases_2/doctor_office.gd b/scripts/ui/ccs_cases_2/doctor_office.gd index 4bc17b6..d00e6cb 100644 --- a/scripts/ui/ccs_cases_2/doctor_office.gd +++ b/scripts/ui/ccs_cases_2/doctor_office.gd @@ -12,9 +12,9 @@ const CHOICE_CROP := Rect2(80.0, 200.0, 1610.0, 485.0) const CHOICES_PER_PAGE := 4 const DIALOGUE_PAGE_MIN_CHARS := 32 const DIALOGUE_PAGE_MAX_CHARS := 82 -const DIALOGUE_BUBBLE_MIN_HEIGHT := 110.0 +const DIALOGUE_BUBBLE_MIN_HEIGHT := 130.0 const DIALOGUE_BUBBLE_MAX_HEIGHT := 240.0 -const DIALOGUE_BUBBLE_VERTICAL_CHROME := 83.0 +const DIALOGUE_BUBBLE_VERTICAL_CHROME := 95.0 @onready var hour_hand: Node2D = $ClockMount/HourHand @onready var minute_hand: Node2D = $ClockMount/MinuteHand @@ -43,6 +43,7 @@ var dialogue_page_index := 0 var dialogue_characters_per_second := 35.0 var dialogue_reveal_complete := false var dialogue_layout_generation := 0 +var dialogue_input_enabled := true func _ready() -> void: @@ -219,7 +220,7 @@ func complete_response_transition() -> void: func _input(event: InputEvent) -> void: - if not dialogue_bubble.visible: + if not dialogue_input_enabled or not dialogue_bubble.visible: return var should_advance := false if event is InputEventMouseButton: @@ -236,6 +237,10 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +func set_dialogue_input_enabled(enabled: bool) -> void: + dialogue_input_enabled = enabled + + func _split_dialogue_pages(text: String) -> Array[String]: var normalized := text.strip_edges() if normalized.is_empty(): diff --git a/scripts/ui/ccs_cases_2_screen.gd b/scripts/ui/ccs_cases_2_screen.gd index 09832aa..a68d1e0 100644 --- a/scripts/ui/ccs_cases_2_screen.gd +++ b/scripts/ui/ccs_cases_2_screen.gd @@ -65,6 +65,11 @@ func on_hide() -> void: _clear_active_scene() +func set_pause_menu_open(open: bool) -> void: + if active_scene != null and active_scene.has_method("set_dialogue_input_enabled"): + active_scene.set_dialogue_input_enabled(not open) + + func compatible_cases(cases: Array) -> Array: return cases.filter(func(item: Variant) -> bool: return typeof(item) == TYPE_DICTIONARY \ diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index 7cda355..f647697 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -126,11 +126,11 @@ func _apply_screen_margins(screen_name: String) -> void: if screens_margin == null or screen_name.is_empty(): return var is_fullscreen_study := screen_name == "player" - var margin := 0 if screen_name == "login" else (12 if is_fullscreen_study else (20 if compact_shell else 44)) + var margin := 0 if screen_name in ["login", "ccs_cases_2"] else (12 if is_fullscreen_study else (20 if compact_shell else 44)) screens_margin.add_theme_constant_override("margin_left", margin) screens_margin.add_theme_constant_override("margin_right", margin) - screens_margin.add_theme_constant_override("margin_top", 0 if screen_name == "login" else (12 if is_fullscreen_study else (20 if compact_shell else 34))) - screens_margin.add_theme_constant_override("margin_bottom", 0 if screen_name == "login" else (12 if is_fullscreen_study else 28)) + screens_margin.add_theme_constant_override("margin_top", 0 if screen_name in ["login", "ccs_cases_2"] else (12 if is_fullscreen_study else (20 if compact_shell else 34))) + screens_margin.add_theme_constant_override("margin_bottom", 0 if screen_name in ["login", "ccs_cases_2"] else (12 if is_fullscreen_study else 28)) func _build_ui() -> void: @@ -582,7 +582,7 @@ func _on_drawer_backdrop_input(event: InputEvent) -> void: func _screen_uses_shell(screen_name: String) -> bool: - return AuthState.is_logged_in and screen_name not in ["login", "legal_consent", "player", "ccs_player"] + return AuthState.is_logged_in and screen_name not in ["login", "legal_consent", "player", "ccs_player", "ccs_cases_2"] func _update_shell_visibility(screen_name: String) -> void: @@ -703,11 +703,15 @@ func _input(event: InputEvent) -> void: _close_drawer() get_viewport().set_input_as_handled() return - if current_screen_name in ["player", "ccs_player", "flashcards_study"]: + if _screen_uses_pause_menu(current_screen_name): _toggle_app_menu() get_viewport().set_input_as_handled() +func _screen_uses_pause_menu(screen_name: String) -> bool: + return screen_name in ["player", "ccs_player", "ccs_cases_2", "flashcards_study"] + + func _refresh_header_controls() -> void: if feedback_button != null: feedback_button.visible = AuthState.is_logged_in and size.x >= 560.0 @@ -875,13 +879,15 @@ func _toggle_app_menu() -> void: _close_app_menu() else: UISoundService.play_sound("open") + _set_current_screen_pause_menu_open(true) app_menu.visible = true app_menu.size = app_menu.custom_minimum_size app_menu.position = (get_viewport_rect().size - app_menu.size) * 0.5 - menu_exit_button.visible = (current_screen_name == "player" and BlockSessionService.block_started) or current_screen_name in ["ccs_player", "flashcards_study"] + menu_exit_button.visible = (current_screen_name == "player" and BlockSessionService.block_started) or current_screen_name in ["ccs_player", "ccs_cases_2", "flashcards_study"] if menu_exit_button.visible: menu_exit_button.text = ( "Abandon CCS Case" if current_screen_name == "ccs_player" + else "Exit CCS Interview" if current_screen_name == "ccs_cases_2" else "Exit Study Session" if current_screen_name == "flashcards_study" else "Leave (timer continues)" if BlockSessionService.session_kind == "simulation" else "Exit Run") @@ -892,9 +898,16 @@ func _toggle_app_menu() -> void: func _close_app_menu() -> void: if app_menu != null: app_menu.visible = false + _set_current_screen_pause_menu_open(false) BlockSessionService.resume_current_question_timer() +func _set_current_screen_pause_menu_open(open: bool) -> void: + var screen = screens.get(current_screen_name) + if screen != null and screen.has_method("set_pause_menu_open"): + screen.set_pause_menu_open(open) + + func _navigate_from_pause(target_screen: String) -> void: var return_screen := current_screen_name show_screen(target_screen, {"return_screen": return_screen}) @@ -911,6 +924,9 @@ func exit_current_run() -> void: if current_screen_name == "flashcards_study": show_screen("flashcards_hub") return + if current_screen_name == "ccs_cases_2": + show_screen("dashboard") + return if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation": BlockSessionService.finish_block() elif BlockSessionService.session_kind == "simulation": diff --git a/tests/ccs_cases_2_ui_test.gd b/tests/ccs_cases_2_ui_test.gd index 5b1fe00..6ab74b9 100644 --- a/tests/ccs_cases_2_ui_test.gd +++ b/tests/ccs_cases_2_ui_test.gd @@ -23,6 +23,16 @@ func _run() -> void: var nav_button := main_screen.sidebar.find_child("NavCcsCases2", true, false) as Button _assert(nav_button != null and nav_button.visible, "UAT should expose a CCS Cases 2 navigation item") _assert(main_screen.screens.has("ccs_cases_2"), "UAT should register the graphical CCS screen") + _assert(not main_screen._screen_uses_shell("ccs_cases_2"), "CCS Cases 2 should hide the application shell") + main_screen._apply_screen_margins("ccs_cases_2") + _assert(main_screen.screens_margin.get_theme_constant("margin_left") == 0, "fullscreen CCS should remove horizontal screen margins") + _assert(main_screen.screens_margin.get_theme_constant("margin_top") == 0, "fullscreen CCS should remove vertical screen margins") + main_screen.current_screen_name = "ccs_cases_2" + _assert(main_screen._screen_uses_pause_menu("ccs_cases_2"), "CCS Cases 2 should route Escape to the pause menu") + main_screen._toggle_app_menu() + _assert(main_screen.app_menu.visible, "the pause menu should open over CCS Cases 2") + _assert(main_screen.menu_exit_button.visible and main_screen.menu_exit_button.text == "Exit CCS Interview", "the CCS pause menu should expose a clear exit action") + main_screen._close_app_menu() main_screen.queue_free() await process_frame ProjectSettings.set_setting("step3/deployment_environment", prior_environment) @@ -94,12 +104,19 @@ func _run() -> void: _assert(_bubble_matches_content(screen.active_scene), "the speech bubble should size itself to the wrapped response page") _assert(screen.active_scene.dialogue_continue_hint.visible, "the continuation prompt should remain visible while text is typing") _assert(screen.active_scene.dialogue_continue_hint.text == "Click to continue", "every dialogue page should use supported continuation wording") + _assert(screen.active_scene.dialogue_text.visible_characters_behavior == TextServer.VC_CHARS_AFTER_SHAPING, "typewriter text should wrap before glyphs are revealed") + var wrapped_line_count: int = screen.active_scene.dialogue_text.get_line_count() var outside_click := _left_click(screen.active_scene.get_global_rect().end + Vector2(50.0, 50.0)) screen.active_scene._input(outside_click) _assert(not screen.active_scene.dialogue_reveal_complete, "clicks outside the doctor scene should not affect dialogue") var scene_click := _left_click(screen.active_scene.get_global_rect().get_center()) + screen.set_pause_menu_open(true) + screen.active_scene._input(scene_click) + _assert(not screen.active_scene.dialogue_reveal_complete, "dialogue should ignore scene clicks while the Escape menu is open") + screen.set_pause_menu_open(false) screen.active_scene._input(scene_click) _assert(screen.active_scene.dialogue_page_index == 0, "clicking during typing should complete rather than skip the current page") + _assert(screen.active_scene.dialogue_text.get_line_count() == wrapped_line_count, "revealing typewriter text should not shift words between lines") _assert(screen.active_scene.dialogue_continue_hint.visible, "completed dialogue pages should invite the learner to continue") await create_timer(1.35).timeout _assert(screen.active_scene.is_patient_dialogue_visible(), "patient dialogue should remain until the learner advances it") @@ -161,6 +178,7 @@ func _run() -> void: var short_height: float = sizing_scene.dialogue_bubble.size.y _assert(is_equal_approx(short_height, sizing_scene.DIALOGUE_BUBBLE_MIN_HEIGHT), "short dialogue should use the minimum bubble height") _assert(sizing_scene.dialogue_bubble.get_global_rect().encloses(sizing_scene.dialogue_continue_hint.get_global_rect()), "the continuation hint should fit inside a minimum-height bubble") + _assert(sizing_scene.dialogue_continue_hint.position.y - sizing_scene.dialogue_text.get_rect().end.y >= 8.0, "the continuation hint should have visible padding above it") sizing_scene.show_patient_dialogue("Line one explains the symptom.\nLine two adds timing.\nLine three adds context.\nLine four concludes.") await process_frame _assert(sizing_scene.dialogue_bubble.size.y > short_height, "multi-line dialogue should increase the bubble height")