427 lines
27 KiB
GDScript
427 lines
27 KiB
GDScript
extends SceneTree
|
|
|
|
var failures := 0
|
|
|
|
|
|
class ScreenHost extends Control:
|
|
var shown_screen := ""
|
|
|
|
func show_screen(screen_name: String, _payload: Variant = null) -> void:
|
|
shown_screen = screen_name
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var prior_environment: String = ProjectSettings.get_setting("step3/deployment_environment", "development")
|
|
ProjectSettings.set_setting("step3/deployment_environment", "uat")
|
|
var main_screen = load("res://scripts/ui/main.gd").new()
|
|
root.add_child(main_screen)
|
|
await process_frame
|
|
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)
|
|
|
|
var service = root.get_node("CCSService")
|
|
var prior_attempt: Dictionary = service.attempt.duplicate(true)
|
|
var prior_cases: Array = service.cases.duplicate(true)
|
|
var prior_origin: String = service.launch_origin
|
|
var host := ScreenHost.new()
|
|
host.size = Vector2(1200.0, 700.0)
|
|
root.add_child(host)
|
|
var screen = load("res://scripts/ui/ccs_cases_2_screen.gd").new()
|
|
screen.main = host
|
|
host.add_child(screen)
|
|
await process_frame
|
|
|
|
service.cases = [
|
|
{
|
|
"externalId": "interactive", "title": "Interactive case", "system": "Endocrine", "difficulty": 2,
|
|
"realTimeMinutes": 20, "interactiveInterviewAvailable": true, "graphicalPresentationAvailable": true,
|
|
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male"}
|
|
},
|
|
{"externalId": "legacy", "title": "Legacy case", "interactiveInterviewAvailable": false}
|
|
]
|
|
service.attempt = {}
|
|
screen.cases_loaded = true
|
|
screen.active_loaded = true
|
|
screen._render_state()
|
|
await process_frame
|
|
_assert(screen.current_view == "case_selection", "CCS Cases 2 should begin with compatible case selection")
|
|
_assert(screen.case_list.get_child_count() == 1, "legacy cases should be excluded from graphical selection")
|
|
var retired_attempt := {
|
|
"caseExternalId": "ccs-gastro-duodenal-ulcer-office-001",
|
|
"graphicalPresentation": {"interviewRevision": 1}
|
|
}
|
|
_assert(screen._is_retired_interview_revision(retired_attempt, {
|
|
"id": "fatigue_course", "patientText": "I feel tired.", "choices": []
|
|
}), "an older authored interview revision should trigger a restart")
|
|
retired_attempt.graphicalPresentation.interviewRevision = 3
|
|
_assert(not screen._is_retired_interview_revision(retired_attempt, {}),
|
|
"the current authored interview revision should remain active")
|
|
|
|
service.attempt = _sample_attempt(false)
|
|
screen._render_state()
|
|
await process_frame
|
|
_assert(screen.current_view == "doctor_office", "an active interactive attempt should open the doctor office")
|
|
_assert(not screen.active_scene.is_patient_dialogue_visible(), "the opening should wait for a clinician dialogue selection")
|
|
_assert(screen.active_scene.choice_container.get_child_count() == 1, "the scene should render server-provided dialogue choices")
|
|
_assert(screen.active_scene.computer_launcher.visible, "the doctor office should expose the simulated computer launcher")
|
|
_assert(screen.active_scene.computer_launcher.icon.resource_path.ends_with("monitor.svg"), "the computer launcher should use the monitor icon")
|
|
screen.active_scene.computer_launcher.pressed.emit()
|
|
_assert(screen.active_scene.is_computer_open(), "the computer launcher should open the simulated desktop")
|
|
_assert(not screen.active_scene.computer_launcher.visible, "the launcher should hide while the computer is open")
|
|
_assert(screen.active_scene.computer_overlay.get_node("Screen").texture.resource_path.ends_with("computer-base.png"), "the overlay should render the supplied computer base")
|
|
_assert(screen.active_scene.computer_overlay.get_node("Screen").stretch_mode == TextureRect.STRETCH_SCALE, "the computer should fill the viewport without side bars")
|
|
_assert(screen.active_scene.vitals_launcher.visible, "the computer desktop should expose a Vitals app")
|
|
_assert(screen.active_scene.medical_history_launcher.visible, "the computer desktop should expose a Medical History app")
|
|
_assert(screen.active_scene.vitals_launcher.text.is_empty() and screen.active_scene.medical_history_launcher.text.is_empty(), "desktop shortcuts should place labels below centered icons")
|
|
_assert(screen.active_scene.vitals_launcher.get_node("Label").text == "Vitals" and screen.active_scene.medical_history_launcher.get_node("Label").text == "Medical History", "desktop shortcut labels should remain readable")
|
|
_assert(screen.active_scene.vitals_launcher.get_theme_stylebox("normal") is StyleBoxEmpty and screen.active_scene.medical_history_launcher.get_theme_stylebox("normal") is StyleBoxEmpty, "desktop shortcuts should not render permanent button tiles")
|
|
_assert(screen.active_scene.medical_history_launcher.position.y > screen.active_scene.vitals_launcher.get_rect().end.y, "desktop shortcuts should form a vertical icon column")
|
|
_assert(screen.active_scene.vitals_launcher.icon.get_width() >= 58 and screen.active_scene.medical_history_launcher.icon.get_width() >= 58, "desktop shortcuts should use high-resolution vector imports")
|
|
screen.active_scene.vitals_launcher.pressed.emit()
|
|
_assert(screen.active_scene.is_vitals_open(), "the Vitals app should open a nurse vital-sign window")
|
|
_assert(screen.active_scene.vitals_header.get_node("Title").text == "Vitals", "the vital window should use the concise Vitals heading")
|
|
_assert(_vital_values(screen.active_scene) == ["36.8 C (98.2 F)", "76/min", "14/min", "122 mm Hg", "76 mm Hg", "99% on room air"], "the Vitals app should render the active attempt data in clinical order")
|
|
_assert(_vital_values_fit(screen.active_scene), "vital values should retain a readable column without character-by-character wrapping")
|
|
screen.active_scene.medical_history_launcher.pressed.emit()
|
|
_assert(screen.active_scene.is_vitals_open() and screen.active_scene.is_medical_history_open(), "both chart windows should be able to remain open")
|
|
_assert(screen.active_scene.medical_history_text.text == "Hyperlipidemia", "Medical History should render the case-authored past medical history")
|
|
_assert(screen.active_scene.medical_history_window.size.y <= 310.0, "Medical History should size closely to its short content (size %s, minimum %s)" % [screen.active_scene.medical_history_window.size, screen.active_scene.medical_history_window.get_combined_minimum_size()])
|
|
_assert(screen.active_scene.vitals_window.size.y <= 410.0, "Vitals should size to its rendered measurement rows")
|
|
_assert(screen.active_scene.medical_history_window.z_index > screen.active_scene.vitals_window.z_index, "the most recently opened chart window should come to the front")
|
|
var original_history_position: Vector2 = screen.active_scene.medical_history_window.position
|
|
screen.active_scene._set_medical_history_window_position(Vector2(90.0, 70.0))
|
|
var moved_history_position: Vector2 = screen.active_scene.medical_history_window.position
|
|
_assert(not moved_history_position.is_equal_approx(original_history_position) \
|
|
and moved_history_position.x >= 0.0 and moved_history_position.y >= 0.0,
|
|
"Medical History should use the shared draggable-window behavior")
|
|
_assert(screen.close_transient_overlay(), "the first Escape should close the topmost Medical History app")
|
|
_assert(not screen.active_scene.is_medical_history_open() and screen.active_scene.is_vitals_open(), "closing Medical History should leave Vitals open")
|
|
screen.active_scene._set_vitals_window_position(Vector2(75.0, 55.0))
|
|
_assert(screen.active_scene.vitals_window.position.is_equal_approx(Vector2(75.0, 55.0)), "the Vitals window should support repositioning by its header")
|
|
screen.active_scene._set_vitals_window_position(Vector2(-500.0, 5000.0))
|
|
var max_vitals_position: Vector2 = screen.active_scene.size - screen.active_scene.vitals_window.size
|
|
_assert(screen.active_scene.vitals_window.position.is_equal_approx(Vector2(0.0, max_vitals_position.y)), "dragging should keep the Vitals window inside the computer viewport")
|
|
_assert(screen.close_transient_overlay(), "the second Escape should close the Vitals app")
|
|
_assert(not screen.active_scene.is_vitals_open() and screen.active_scene.is_computer_open(), "closing Vitals should return to the computer desktop")
|
|
_assert(screen.close_transient_overlay(), "the third Escape should close the computer")
|
|
_assert(not screen.active_scene.is_computer_open(), "closing the transient overlay should restore the doctor office")
|
|
_assert(not screen.close_transient_overlay(), "the fourth Escape should be available to open the pause menu")
|
|
screen.active_scene.open_computer()
|
|
screen.active_scene.open_vitals()
|
|
screen.active_scene.open_medical_history()
|
|
screen.active_scene.computer_close_button.pressed.emit()
|
|
_assert(not screen.active_scene.is_computer_open() and not screen.active_scene.is_vitals_open() \
|
|
and not screen.active_scene.is_medical_history_open(),
|
|
"the computer close button should close every child chart window")
|
|
_assert(is_equal_approx(screen.DESIGN_SIZE.x / screen.DESIGN_SIZE.y, 16.0 / 9.0), "the graphical CCS stage should use a true 16:9 design canvas")
|
|
_assert(screen.stage_root.size.is_equal_approx(screen.size), "the graphical stage should fill the available viewport")
|
|
_assert(screen.stage_root.position.is_equal_approx(Vector2.ZERO), "the graphical stage should not introduce letterbox margins")
|
|
var stale_graphical_attempt := _sample_attempt(false)
|
|
stale_graphical_attempt.experienceMode = "graphical_interview"
|
|
stale_graphical_attempt.openingCompleted = true
|
|
service.attempt = stale_graphical_attempt
|
|
screen._render_state()
|
|
await process_frame
|
|
_assert(screen.current_view == "doctor_office", "legacy completion flags should not block a graphical interview")
|
|
service.attempt = _sample_branching_attempt()
|
|
screen._render_state()
|
|
await process_frame
|
|
_assert(screen.active_scene.is_patient_dialogue_visible(), "a selected topic should display the patient's response")
|
|
_assert(not screen.active_scene.choice_container.visible, "the next choices should wait until the patient finishes speaking")
|
|
_assert(screen.active_scene.dialogue_pages.size() > 1, "long patient responses should split into readable dialogue pages")
|
|
_assert(_label_content_fits(screen.active_scene.dialogue_text), "the first patient response page should fit inside its speech bubble")
|
|
_assert(is_equal_approx(screen.active_scene.dialogue_text.offset_left, 96.0), "patient dialogue should retain the increased left inset")
|
|
_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()
|
|
screen.active_scene.open_computer()
|
|
var computer_blocked_page: int = screen.active_scene.dialogue_page_index
|
|
var computer_click := _left_click(screen.active_scene.get_global_rect().get_center())
|
|
screen.active_scene._input(computer_click)
|
|
_assert(screen.active_scene.dialogue_page_index == computer_blocked_page and not screen.active_scene.dialogue_reveal_complete, "the open computer should block scene-wide dialogue clicks")
|
|
screen.active_scene.computer_close_button.pressed.emit()
|
|
_assert(not screen.active_scene.is_computer_open(), "the computer close button should return to the interview")
|
|
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")
|
|
_assert(not screen.active_scene.choice_container.visible, "questions must not appear on a timer")
|
|
while screen.active_scene.dialogue_page_index < screen.active_scene.dialogue_pages.size() - 1:
|
|
screen.active_scene._input(scene_click)
|
|
await process_frame
|
|
_assert(_label_content_fits(screen.active_scene.dialogue_text), "every patient response page should fit inside its speech bubble")
|
|
screen.active_scene.complete_dialogue_reveal()
|
|
screen.active_scene._input(scene_click)
|
|
await process_frame
|
|
_assert(not screen.active_scene.is_patient_dialogue_visible(), "questions should replace patient dialogue after the final learner click")
|
|
_assert(screen.active_scene.choice_container.get_child_count() == 4, "the first question page should contain four choices")
|
|
screen.active_scene._input(scene_click)
|
|
_assert(screen.active_scene.choice_container.get_child_count() == 4, "scene clicks should not affect explicit question controls")
|
|
_assert(screen.active_scene.choice_pager.visible, "eight choices should display paging controls")
|
|
_assert(screen.active_scene.choice_page == 0, "new question sets should begin on the first page")
|
|
_assert(_visible_choice_ids(screen.active_scene) == ["topic_0", "topic_1", "topic_2", "topic_3"], "the first page should preserve choice order and IDs")
|
|
_assert(_choice_labels_fit(screen.active_scene), "first-page question text should fit within every button")
|
|
screen.active_scene._change_choice_page(1)
|
|
await process_frame
|
|
_assert(screen.active_scene.choice_page == 1, "Next should advance to the second question page")
|
|
_assert(screen.active_scene.choice_container.get_child_count() == 4, "the second question page should contain the remaining choices")
|
|
_assert(_visible_choice_ids(screen.active_scene) == ["topic_4", "topic_5", "topic_6", "topic_7"], "the second page should preserve choice order and IDs")
|
|
_assert(_choice_labels_fit(screen.active_scene), "second-page question text should fit within every button")
|
|
screen.active_scene._change_choice_page(-1)
|
|
_assert(screen.active_scene.choice_page == 0, "Previous should return to the first question page")
|
|
|
|
service.attempt = _sample_attempt(true)
|
|
screen._on_attempt_changed()
|
|
await process_frame
|
|
_assert(screen.active_scene.dialogue_text.text == "Okay, doctor.", "attempt updates should advance the rendered patient response")
|
|
_assert(not screen.active_scene.choice_container.visible, "the terminal action should wait for the closing response")
|
|
screen.active_scene.complete_dialogue_reveal()
|
|
screen.active_scene.advance_dialogue()
|
|
_assert(screen.active_scene.choice_container.get_child_count() == 1, "terminal dialogue should expose the management handoff")
|
|
_assert(not screen.active_scene.choice_pager.visible, "a single terminal action should not show paging controls")
|
|
var terminal_choice := screen.active_scene.choice_container.get_child(0) as Control
|
|
_assert(terminal_choice.size.x <= 360.0, "a single terminal choice should remain a compact centered card")
|
|
_assert((terminal_choice.get_child(0) as Label).text == "Begin Physical Examination",
|
|
"terminal dialogue should lead into the graphical physical examination")
|
|
screen.active_scene.interview_continue_requested.emit()
|
|
await process_frame
|
|
_assert(screen.current_view == "physical_examination", "the examination should remain inside CCS Cases 2")
|
|
_assert(screen.active_scene.is_physical_exam_visible(), "the physical examination panel should be visible")
|
|
_assert(screen.active_scene.physical_exam_grid.columns == 2, "exam components should use two responsive columns")
|
|
_assert(screen.active_scene.physical_exam_grid.get_child_count() == 12, "all twelve examination components should be available")
|
|
_assert(not screen.active_scene.physical_exam_finish_button.disabled, "Finish Case should remain available before every exam is complete")
|
|
screen.active_scene.set_physical_exam_pending("abdomen")
|
|
_assert(screen.active_scene.physical_exam_finish_button.disabled, "case completion should pause while an exam command is pending")
|
|
_assert(screen.active_scene.physical_exam_buttons.values().all(func(button): return button.disabled),
|
|
"exam controls should disable while one component is submitted")
|
|
var examined_attempt := _sample_attempt(true)
|
|
examined_attempt.chart = [{"code": "abdomen", "title": "Abdomen", "text": "Mild epigastric tenderness."}]
|
|
service.attempt = examined_attempt
|
|
screen._on_attempt_changed()
|
|
await process_frame
|
|
_assert(screen.current_view == "physical_examination", "attempt refreshes should preserve the physical examination phase")
|
|
_assert(screen.active_scene.physical_exam_buttons.abdomen.text.begins_with("[Done]"), "completed exams should be marked")
|
|
_assert(screen.active_scene.physical_exam_buttons.abdomen.disabled, "completed exams should not be repeated")
|
|
_assert(screen.active_scene.physical_exam_finding_text.text == "Mild epigastric tenderness.", "the returned chart finding should be displayed")
|
|
|
|
service.attempt = {"id": "classic-attempt", "state": "active", "openingCompleted": false, "interview": {}}
|
|
host.shown_screen = ""
|
|
screen._on_attempt_changed()
|
|
await process_frame
|
|
_assert(screen.current_view == "case_selection", "unsupported active attempts should remain outside the classic player")
|
|
_assert(host.shown_screen.is_empty(), "CCS Cases 2 should not trigger classic opening overlays")
|
|
|
|
service.attempt = _sample_graphical_debrief()
|
|
screen._on_attempt_changed()
|
|
await process_frame
|
|
_assert(screen.current_view == "interview_debrief", "a completed graphical interview should remain in CCS Cases 2")
|
|
_assert(screen.active_scene.find_child("InterviewDebrief", true, false) != null, "the graphical case should render its debrief")
|
|
_assert(not screen.active_scene.computer_launcher.visible, "the computer launcher should stay hidden during the interview debrief")
|
|
|
|
var sizing_scene = load("res://scenes/ccs_cases_2/doctor_office.tscn").instantiate()
|
|
host.add_child(sizing_scene)
|
|
await process_frame
|
|
sizing_scene.show_patient_dialogue("Short response.")
|
|
await process_frame
|
|
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")
|
|
_assert(sizing_scene.dialogue_bubble.size.y <= sizing_scene.DIALOGUE_BUBBLE_MAX_HEIGHT, "dialogue bubble growth should remain bounded")
|
|
_assert(_bubble_matches_content(sizing_scene), "the expanded bubble should contain all wrapped text")
|
|
sizing_scene.complete_dialogue_reveal()
|
|
_assert(sizing_scene.dialogue_bubble.get_global_rect().encloses(sizing_scene.dialogue_continue_hint.get_global_rect()), "the continuation hint should remain inside the resized bubble")
|
|
sizing_scene.queue_free()
|
|
|
|
screen.on_hide()
|
|
await process_frame
|
|
_assert(screen.active_scene == null, "hiding CCS Cases 2 should release the doctor scene")
|
|
host.queue_free()
|
|
service.attempt = prior_attempt
|
|
service.cases = prior_cases
|
|
service.launch_origin = prior_origin
|
|
await process_frame
|
|
quit(1 if failures > 0 else 0)
|
|
|
|
|
|
func _sample_attempt(completed: bool) -> Dictionary:
|
|
var node := {
|
|
"id": "done" if completed else "greeting",
|
|
"patientText": "Okay, doctor." if completed else "Hello, doctor.",
|
|
"isTerminal": completed,
|
|
"suppressPatientText": not completed,
|
|
"choices": [] if completed else [{"id": "ask_reason", "text": "What brings you in?", "nextNodeId": "done"}]
|
|
}
|
|
return {
|
|
"id": "attempt-1", "state": "active", "revision": 1,
|
|
"simulatedStartUtc": "2026-08-05T09:00:00Z", "simulatedMinute": 0,
|
|
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male", "interviewRevision": 3},
|
|
"medicalHistory": "Hyperlipidemia",
|
|
"vitals": {
|
|
"temperature": "36.8 C (98.2 F)", "bloodPressure": "122/76 mm Hg",
|
|
"pulse": "76/min", "respirations": "14/min", "oxygenSaturation": "99% on room air"
|
|
},
|
|
"openingCompleted": completed,
|
|
"experienceMode": "graphical_interview",
|
|
"availableExams": _sample_exams(), "chart": [],
|
|
"interview": {"currentNodeId": node.id, "currentNode": node, "visitedChoiceIds": [], "completed": completed}
|
|
}
|
|
|
|
|
|
func _sample_exams() -> Array:
|
|
var definitions := [
|
|
["physical_appearance", "Physical Appearance"], ["skin", "Skin"], ["breasts", "Breasts"],
|
|
["lymph_nodes", "Lymph Nodes"], ["heent_neck", "HEENT / Neck"], ["chest_lungs", "Chest / Lungs"],
|
|
["heart_cardiovascular", "Heart / Cardiovascular"], ["abdomen", "Abdomen"], ["genitalia", "Genitalia"],
|
|
["rectal", "Rectal"], ["extremities_spine", "Extremities / Spine"], ["neuro_psych", "Neuro / Psych"]
|
|
]
|
|
var exams: Array = []
|
|
for definition in definitions:
|
|
exams.append({"code": definition[0], "name": definition[1], "durationMinutes": 1})
|
|
return exams
|
|
|
|
|
|
func _sample_graphical_debrief() -> Dictionary:
|
|
return {
|
|
"id": "attempt-2", "caseExternalId": "interactive", "state": "completed", "revision": 7,
|
|
"experienceMode": "graphical_interview", "simulatedStartUtc": "2026-08-05T09:00:00Z", "simulatedMinute": 0,
|
|
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male"},
|
|
"interviewDebrief": {
|
|
"likelyDiagnosis": "Probable duodenal ulcer", "summary": "A focused history supports the assessment.",
|
|
"teachingPoints": ["Characterize dizziness.", "Screen for bleeding."]
|
|
},
|
|
"score": {"percent": 85.0}
|
|
}
|
|
|
|
|
|
func _sample_branching_attempt() -> Dictionary:
|
|
var option_texts := [
|
|
"Tell me more about the dizziness. Does the room spin, and have you fainted?",
|
|
"How has the fatigue affected you, and how have you been sleeping and eating?",
|
|
"You mentioned your stomach. Please describe the discomfort and when it occurs.",
|
|
"Have you had black stools, blood in vomit or stool, trouble swallowing, persistent vomiting, weight loss, or severe sudden pain?",
|
|
"What medicines do you take, including aspirin, ibuprofen, naproxen, or other over-the-counter products?",
|
|
"Do you have prior stomach problems, other medical conditions, surgeries, or medication allergies?",
|
|
"Do you smoke, drink alcohol, or use recreational drugs?",
|
|
"Any fever, shortness of breath, headaches, bowel changes, or urinary symptoms?"
|
|
]
|
|
var choices: Array = []
|
|
for index in 8:
|
|
choices.append({"id": "topic_%d" % index, "text": option_texts[index], "nextNodeId": "topic_%d" % index})
|
|
return {
|
|
"id": "attempt-1", "state": "active", "revision": 2,
|
|
"simulatedStartUtc": "2026-08-05T09:00:00Z", "simulatedMinute": 0,
|
|
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male"},
|
|
"openingCompleted": false,
|
|
"interview": {
|
|
"currentNodeId": "chief_concern",
|
|
"currentNode": {
|
|
"id": "chief_concern",
|
|
"patientText": "For the past few weeks, I have felt worn out, and every so often I get lightheaded—especially when I stand up quickly.",
|
|
"isTerminal": false,
|
|
"choices": choices
|
|
},
|
|
"visitedChoiceIds": ["chief_concern"], "completed": false
|
|
}
|
|
}
|
|
|
|
|
|
func _visible_choice_ids(scene: Control) -> Array:
|
|
var ids: Array = []
|
|
for child in scene.choice_container.get_children():
|
|
ids.append(str(child.get_meta("choice_id", "")))
|
|
return ids
|
|
|
|
|
|
func _vital_values(scene: Control) -> Array:
|
|
var values: Array = []
|
|
for index in range(1, scene.vitals_rows.get_child_count(), 2):
|
|
values.append(scene.vitals_rows.get_child(index).text)
|
|
return values
|
|
|
|
|
|
func _vital_values_fit(scene: Control) -> bool:
|
|
for index in range(1, scene.vitals_rows.get_child_count(), 2):
|
|
var label := scene.vitals_rows.get_child(index) as Label
|
|
if label == null or label.size.x < 220.0 or label.get_line_count() != 1:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _choice_labels_fit(scene: Control) -> bool:
|
|
for button in scene.choice_container.get_children():
|
|
var label := button.get_child(0) as Label
|
|
if label == null or not _label_content_fits(label):
|
|
return false
|
|
if not scene.choice_area.get_global_rect().encloses(button.get_global_rect()):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _label_content_fits(label: Control) -> bool:
|
|
if label is RichTextLabel:
|
|
return label.get_content_height() <= label.size.y + 0.5
|
|
var plain_label := label as Label
|
|
if plain_label == null:
|
|
return false
|
|
var font := plain_label.get_theme_font("font")
|
|
var font_size := plain_label.get_theme_font_size("font_size")
|
|
var content_height := float(plain_label.get_line_count()) * font.get_height(font_size)
|
|
return content_height <= plain_label.size.y + 0.5
|
|
|
|
|
|
func _bubble_matches_content(scene: Control) -> bool:
|
|
var required_height: float = scene.dialogue_text.get_content_height() + scene.DIALOGUE_BUBBLE_VERTICAL_CHROME
|
|
var expected_height := clampf(
|
|
required_height, scene.DIALOGUE_BUBBLE_MIN_HEIGHT, scene.DIALOGUE_BUBBLE_MAX_HEIGHT)
|
|
return is_equal_approx(scene.dialogue_bubble.size.y, expected_height) \
|
|
and _label_content_fits(scene.dialogue_text)
|
|
|
|
|
|
func _left_click(position: Vector2) -> InputEventMouseButton:
|
|
var event := InputEventMouseButton.new()
|
|
event.button_index = MOUSE_BUTTON_LEFT
|
|
event.pressed = true
|
|
event.position = position
|
|
return event
|
|
|
|
|
|
func _assert(condition: bool, message: String) -> void:
|
|
if condition:
|
|
return
|
|
failures += 1
|
|
push_error(message)
|