Add graphical CCS physical examination

This commit is contained in:
hz
2026-08-10 03:27:45 -05:00
parent 579e1e9be4
commit 07516784d2
5 changed files with 306 additions and 15 deletions
+183 -1
View File
@@ -2,6 +2,8 @@ extends Control
signal choice_selected(choice_id: String)
signal interview_continue_requested
signal physical_exam_selected(exam_code: String)
signal physical_exam_finish_requested
signal debrief_return_requested
signal debrief_repeat_requested
@@ -48,6 +50,13 @@ var blink_tween: Tween
var dialogue_tween: Tween
var clock_seconds := 9.0 * 60.0 * 60.0
var debrief_panel: PanelContainer
var physical_exam_panel: PanelContainer
var physical_exam_grid: GridContainer
var physical_exam_finding_title: Label
var physical_exam_finding_text: Label
var physical_exam_status: Label
var physical_exam_finish_button: Button
var physical_exam_buttons := {}
var experience_mode := "management"
var pending_choices: Array = []
var available_choices: Array = []
@@ -208,7 +217,7 @@ func configure_interview(node: Dictionary) -> void:
if bool(node.get("isTerminal", false)):
next_choices = [{
"id": "__continue",
"text": "Finish Interview" if experience_mode == "graphical_interview" else "Continue to Patient Management"
"text": "Begin Physical Examination" if experience_mode == "graphical_interview" else "Continue to Patient Management"
}]
else:
next_choices = node.get("choices", [])
@@ -719,7 +728,180 @@ func _on_dialogue_choice_pressed(choice_id: String) -> void:
choice_selected.emit(choice_id)
func show_physical_exam(exams: Array, chart: Array, pending_code: String = "") -> void:
hide_patient_dialogue()
hide_dialogue_choices()
computer_available = true
computer_launcher.visible = not computer_overlay.visible
_ensure_physical_exam_panel()
physical_exam_panel.visible = true
for child in physical_exam_grid.get_children():
physical_exam_grid.remove_child(child)
child.queue_free()
physical_exam_buttons.clear()
var completed := {}
for raw_entry in chart:
var entry: Dictionary = raw_entry
completed[str(entry.get("code", ""))] = entry
var exam_codes := {}
for raw_exam in exams:
var exam: Dictionary = raw_exam
var code := str(exam.get("code", ""))
if code.is_empty():
continue
exam_codes[code] = true
var button := Button.new()
var is_complete := completed.has(code)
button.custom_minimum_size = Vector2(0.0, 48.0)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.text = "%s%s" % ["[Done] " if is_complete else "", str(exam.get("name", code.capitalize()))]
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.disabled = is_complete or not pending_code.is_empty()
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.pressed.connect(_on_physical_exam_pressed.bind(code))
_style_physical_exam_button(button, is_complete)
physical_exam_grid.add_child(button)
physical_exam_buttons[code] = button
var latest_finding: Dictionary = {}
for index in range(chart.size() - 1, -1, -1):
var entry: Dictionary = chart[index]
if exam_codes.has(str(entry.get("code", ""))):
latest_finding = entry
break
if latest_finding.is_empty():
physical_exam_finding_title.text = "Examination Findings"
physical_exam_finding_text.text = "Select an examination component to perform it and review the finding."
else:
physical_exam_finding_title.text = str(latest_finding.get("title", "Examination Finding"))
physical_exam_finding_text.text = str(latest_finding.get("text", "No finding was reported."))
set_physical_exam_pending(pending_code)
func set_physical_exam_pending(exam_code: String) -> void:
var pending := not exam_code.is_empty()
for code in physical_exam_buttons:
var button: Button = physical_exam_buttons[code]
button.disabled = pending or button.text.begins_with("[Done]")
physical_exam_finish_button.disabled = pending
physical_exam_status.text = "Performing %s..." % _physical_exam_name(exam_code) if pending else ""
func is_physical_exam_visible() -> bool:
return physical_exam_panel != null and physical_exam_panel.visible
func show_physical_exam_error(message: String) -> void:
set_physical_exam_pending("")
physical_exam_status.text = message
func _ensure_physical_exam_panel() -> void:
if physical_exam_panel != null and is_instance_valid(physical_exam_panel):
return
physical_exam_panel = PanelContainer.new()
physical_exam_panel.name = "PhysicalExamination"
physical_exam_panel.z_index = 50
add_child(physical_exam_panel)
physical_exam_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
physical_exam_panel.offset_left = 44.0
physical_exam_panel.offset_top = 28.0
physical_exam_panel.offset_right = -44.0
physical_exam_panel.offset_bottom = -28.0
var panel_style := StyleBoxFlat.new()
panel_style.bg_color = Color(0.96, 0.97, 0.98, 0.97)
panel_style.border_color = Color(0.12, 0.22, 0.28, 0.9)
panel_style.set_border_width_all(2)
panel_style.set_corner_radius_all(14)
physical_exam_panel.add_theme_stylebox_override("panel", panel_style)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 26)
margin.add_theme_constant_override("margin_top", 22)
margin.add_theme_constant_override("margin_right", 26)
margin.add_theme_constant_override("margin_bottom", 22)
physical_exam_panel.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 10)
margin.add_child(content)
var heading := Label.new()
heading.text = "Physical Examination"
heading.add_theme_font_size_override("font_size", 30)
heading.add_theme_color_override("font_color", Color(0.055, 0.105, 0.145))
content.add_child(heading)
var instructions := Label.new()
instructions.text = "Select one component at a time. Completed examinations are marked below."
instructions.add_theme_font_size_override("font_size", 17)
instructions.add_theme_color_override("font_color", Color(0.18, 0.25, 0.31))
content.add_child(instructions)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
content.add_child(scroll)
physical_exam_grid = GridContainer.new()
physical_exam_grid.name = "ExamComponents"
physical_exam_grid.columns = 2
physical_exam_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
physical_exam_grid.add_theme_constant_override("h_separation", 12)
physical_exam_grid.add_theme_constant_override("v_separation", 8)
scroll.add_child(physical_exam_grid)
var divider := HSeparator.new()
content.add_child(divider)
physical_exam_finding_title = Label.new()
physical_exam_finding_title.add_theme_font_size_override("font_size", 21)
physical_exam_finding_title.add_theme_color_override("font_color", Color(0.08, 0.19, 0.27))
content.add_child(physical_exam_finding_title)
physical_exam_finding_text = Label.new()
physical_exam_finding_text.custom_minimum_size.y = 64.0
physical_exam_finding_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
physical_exam_finding_text.add_theme_font_size_override("font_size", 17)
physical_exam_finding_text.add_theme_color_override("font_color", Color(0.06, 0.1, 0.14))
content.add_child(physical_exam_finding_text)
var actions := HBoxContainer.new()
actions.alignment = BoxContainer.ALIGNMENT_END
actions.add_theme_constant_override("separation", 12)
content.add_child(actions)
physical_exam_status = Label.new()
physical_exam_status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
physical_exam_status.add_theme_font_size_override("font_size", 16)
physical_exam_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
actions.add_child(physical_exam_status)
physical_exam_finish_button = Button.new()
physical_exam_finish_button.text = "Finish Case"
physical_exam_finish_button.custom_minimum_size = Vector2(170.0, 48.0)
physical_exam_finish_button.pressed.connect(func(): physical_exam_finish_requested.emit())
actions.add_child(physical_exam_finish_button)
func _style_physical_exam_button(button: Button, complete: bool) -> void:
var style := StyleBoxFlat.new()
style.bg_color = Color(0.88, 0.94, 0.97, 0.96) if complete else Color(1.0, 1.0, 1.0, 0.96)
style.border_color = Color(0.19, 0.52, 0.43) if complete else Color(0.45, 0.54, 0.61)
style.set_border_width_all(1)
style.set_corner_radius_all(8)
style.set_content_margin_all(12.0)
var hover := style.duplicate() as StyleBoxFlat
hover.border_color = Color(0.16, 0.45, 0.68)
button.add_theme_stylebox_override("normal", style)
button.add_theme_stylebox_override("disabled", style)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", hover)
button.add_theme_font_size_override("font_size", 17)
button.add_theme_color_override("font_color", Color(0.06, 0.12, 0.17))
button.add_theme_color_override("font_disabled_color", Color(0.12, 0.31, 0.26) if complete else Color(0.42, 0.47, 0.51))
func _physical_exam_name(code: String) -> String:
if physical_exam_buttons.has(code):
return str(physical_exam_buttons[code].text).trim_prefix("[Done] ")
return "examination"
func _on_physical_exam_pressed(code: String) -> void:
physical_exam_selected.emit(code)
func show_debrief(debrief: Dictionary, score: Dictionary) -> void:
if physical_exam_panel != null and is_instance_valid(physical_exam_panel):
physical_exam_panel.visible = false
computer_available = false
close_computer()
computer_launcher.visible = false
+55 -11
View File
@@ -18,6 +18,7 @@ var current_view := ""
var cases_loaded := false
var active_loaded := false
var pending_action := ""
var pending_exam_code := ""
var restart_case_external_id := ""
@@ -89,6 +90,7 @@ func _on_cases_changed() -> void:
func _on_attempt_changed() -> void:
active_loaded = true
pending_action = ""
pending_exam_code = ""
_render_state()
@@ -127,6 +129,9 @@ func _render_attempt(attempt: Dictionary) -> void:
and bool(attempt.get("openingCompleted", false)) and not bool(node.get("isTerminal", false)):
_show_unsupported_attempt("This case has progressed beyond the graphical interview.")
return
if current_view == "physical_examination" and bool(node.get("isTerminal", false)):
_show_physical_examination(attempt)
return
_show_doctor_office(node, attempt)
@@ -222,7 +227,9 @@ func _show_doctor_office(node: Dictionary, attempt: Dictionary = {}) -> void:
if active_scene == null:
_set_active_scene(DOCTOR_SCENE.instantiate(), "doctor_office")
active_scene.choice_selected.connect(_submit_dialogue_choice)
active_scene.interview_continue_requested.connect(_handoff_to_classic_player)
active_scene.interview_continue_requested.connect(_begin_physical_examination)
active_scene.physical_exam_selected.connect(_submit_physical_exam)
active_scene.physical_exam_finish_requested.connect(_finish_physical_examination)
active_scene.debrief_return_requested.connect(_return_to_cases)
active_scene.debrief_repeat_requested.connect(_repeat_interview)
selection_view.visible = false
@@ -246,15 +253,48 @@ func _submit_dialogue_choice(choice_id: String) -> void:
CCSService.send_dialogue_choice(choice_id)
func _handoff_to_classic_player() -> void:
if str(CCSService.attempt.get("experienceMode", "")) == "graphical_interview":
pending_action = "finishing_interview"
active_scene.set_choices_pending(true)
CCSService.send_command("exit_case")
func _begin_physical_examination() -> void:
if not pending_action.is_empty():
return
current_view = "handoff"
if main != null:
main.show_screen("ccs_player")
_show_physical_examination(CCSService.attempt)
func _show_physical_examination(attempt: Dictionary) -> void:
if active_scene == null:
_set_active_scene(DOCTOR_SCENE.instantiate(), "physical_examination")
active_scene.physical_exam_selected.connect(_submit_physical_exam)
active_scene.physical_exam_finish_requested.connect(_finish_physical_examination)
active_scene.debrief_return_requested.connect(_return_to_cases)
active_scene.debrief_repeat_requested.connect(_repeat_interview)
selection_view.visible = false
stage_root.visible = true
active_scene.configure_experience_mode("graphical_interview")
active_scene.configure_presentation(
attempt.get("graphicalPresentation", {}),
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")))
active_scene.show_physical_exam(
attempt.get("availableExams", []), attempt.get("chart", []), pending_exam_code)
current_view = "physical_examination"
func _submit_physical_exam(exam_code: String) -> void:
if not pending_action.is_empty() or exam_code.is_empty():
return
pending_action = "physical_exam"
pending_exam_code = exam_code
active_scene.set_physical_exam_pending(exam_code)
CCSService.send_command("exam", exam_code)
func _finish_physical_examination() -> void:
if not pending_action.is_empty():
return
pending_action = "finishing_exam"
active_scene.set_physical_exam_pending("finishing_case")
CCSService.send_command("exit_case")
func _render_debrief(attempt: Dictionary) -> void:
@@ -329,7 +369,11 @@ func _on_request_failed(message: String) -> void:
retry_button.visible = true
return
if active_scene != null:
active_scene.set_choices_pending(false)
if current_view == "physical_examination":
pending_exam_code = ""
active_scene.show_physical_exam_error(message)
else:
active_scene.set_choices_pending(false)
else:
status_label.text = message
retry_button.visible = failed_action == "loading"
@@ -354,7 +398,7 @@ func _clear_active_scene() -> void:
active_scene.get_parent().remove_child(active_scene)
active_scene.queue_free()
active_scene = null
if current_view == "doctor_office":
if current_view in ["doctor_office", "physical_examination"]:
current_view = ""