Files
step3/scripts/ui/ccs_cases_2_screen.gd
T

475 lines
16 KiB
GDScript

extends Control
const Brand := preload("res://scripts/ui/medmind_style.gd")
const Style := preload("res://scripts/ui/ui_style.gd")
const DOCTOR_SCENE := preload("res://scenes/ccs_cases_2/doctor_office.tscn")
const DESIGN_SIZE := Vector2(1600.0, 900.0)
const FOCUSED_INTERVIEW_CASE_ID := "ccs-gastro-duodenal-ulcer-office-001"
const FOCUSED_INTERVIEW_REVISION := 4
var main
var stage_root: Control
var active_scene: Control
var selection_view: VBoxContainer
var case_list: VBoxContainer
var status_label: Label
var retry_button: Button
var current_view := ""
var cases_loaded := false
var active_loaded := false
var pending_action := ""
var pending_exam_code := ""
var restart_case_external_id := ""
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
clip_contents = true
_build_selection_view()
stage_root = Control.new()
stage_root.name = "GraphicalCcsStage"
stage_root.size = DESIGN_SIZE
stage_root.visible = false
add_child(stage_root)
_connect_service()
resized.connect(_fit_stage)
_fit_stage()
func _connect_service() -> void:
if not CCSService.cases_changed.is_connected(_on_cases_changed):
CCSService.cases_changed.connect(_on_cases_changed)
if not CCSService.attempt_changed.is_connected(_on_attempt_changed):
CCSService.attempt_changed.connect(_on_attempt_changed)
if not CCSService.request_failed.is_connected(_on_request_failed):
CCSService.request_failed.connect(_on_request_failed)
if not CCSService.orders_changed.is_connected(_on_orders_changed):
CCSService.orders_changed.connect(_on_orders_changed)
if not CCSService.attempt_abandoned.is_connected(_on_attempt_abandoned):
CCSService.attempt_abandoned.connect(_on_attempt_abandoned)
func on_show(_payload: Variant = null) -> void:
CCSService.launch_origin = "ccs_cases_2"
restart_case_external_id = ""
cases_loaded = false
active_loaded = false
pending_action = "loading"
_show_selection("Loading interactive cases...")
CCSService.load_cases()
CCSService.load_active()
func on_hide() -> void:
restart_case_external_id = ""
_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 close_transient_overlay() -> bool:
if active_scene != null and active_scene.has_method("close_transient_overlay"):
return bool(active_scene.close_transient_overlay())
return false
func compatible_cases(cases: Array) -> Array:
return cases.filter(func(item: Variant) -> bool:
return typeof(item) == TYPE_DICTIONARY \
and bool(item.get("interactiveInterviewAvailable", false)) \
and bool(item.get("graphicalPresentationAvailable", false)) \
and str(item.get("graphicalPresentation", {}).get("sceneKey", "")) == "doctor_office")
func _on_cases_changed() -> void:
cases_loaded = true
_render_state()
func _on_attempt_changed() -> void:
active_loaded = true
var completed_action := pending_action
var completed_exam_code := pending_exam_code
pending_action = ""
pending_exam_code = ""
if completed_action == "physical_exam" and active_scene != null \
and current_view == "doctor_office":
_refresh_exam_result(CCSService.attempt, completed_exam_code)
return
if completed_action in ["orders", "cancel_order", "advance", "enter_final_orders"] \
and active_scene != null and current_view == "doctor_office":
_refresh_case_workspace(CCSService.attempt)
return
_render_state()
func _refresh_exam_result(attempt: Dictionary, exam_code: String) -> void:
_refresh_case_workspace(attempt)
active_scene.show_exam_result(attempt.get("chart", []), exam_code)
func _refresh_case_workspace(attempt: Dictionary) -> void:
active_scene.configure_presentation(
attempt.get("graphicalPresentation", {}),
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
active_scene.configure_examinations(attempt.get("availableExams", []), attempt.get("chart", []))
active_scene.configure_orders(attempt)
func _render_state() -> void:
if not visible:
return
if not active_loaded:
return
var attempt: Dictionary = CCSService.attempt
if not attempt.is_empty() and str(attempt.get("state", "")) == "completed" \
and str(attempt.get("experienceMode", "")) == "graphical_interview":
_render_debrief(attempt)
return
if not attempt.is_empty() and str(attempt.get("state", "")) in ["active", "final_orders"]:
_render_attempt(attempt)
elif cases_loaded and active_loaded:
_render_case_selection()
func _render_attempt(attempt: Dictionary) -> void:
if str(attempt.get("state", "")) not in ["active", "final_orders"]:
_show_unsupported_attempt("This active case is not available in the graphical interview yet.")
return
var interview: Dictionary = attempt.get("interview", {})
if interview.is_empty():
_show_unsupported_attempt("This active case does not include a graphical dialogue interview.")
return
var node: Dictionary = interview.get("currentNode", {})
if node.is_empty():
_show_unsupported_attempt("The graphical interview could not load its current dialogue.")
return
if _is_retired_interview_revision(attempt, node):
_restart_with_current_interview(attempt)
return
if str(attempt.get("experienceMode", "management")) != "graphical_interview" \
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
_show_doctor_office(node, attempt)
func _build_selection_view() -> void:
selection_view = VBoxContainer.new()
selection_view.name = "CaseSelection"
selection_view.add_theme_constant_override("separation", 16)
add_child(selection_view)
selection_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var heading := Label.new()
heading.text = "Choose an Interactive CCS Case"
Brand.apply_font(heading)
heading.add_theme_font_size_override("font_size", 28)
selection_view.add_child(heading)
var description := Label.new()
description.text = "Interview the patient by selecting the questions you want to ask."
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Brand.apply_font(description)
description.add_theme_font_size_override("font_size", 16)
selection_view.add_child(description)
status_label = Label.new()
status_label.name = "CaseSelectionStatus"
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Brand.apply_font(status_label)
selection_view.add_child(status_label)
retry_button = Button.new()
retry_button.text = "Retry"
retry_button.visible = false
retry_button.custom_minimum_size = Vector2(120, 42)
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_primary_button(retry_button)
retry_button.pressed.connect(_retry_load)
selection_view.add_child(retry_button)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
selection_view.add_child(scroll)
case_list = VBoxContainer.new()
case_list.name = "InteractiveCaseList"
case_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
case_list.add_theme_constant_override("separation", 10)
scroll.add_child(case_list)
func _show_selection(message: String = "") -> void:
_clear_active_scene()
current_view = "case_selection"
selection_view.visible = true
stage_root.visible = false
status_label.text = message
retry_button.visible = false
func _show_unsupported_attempt(message: String) -> void:
_show_selection(message)
_clear_children(case_list)
status_label.text += " Finish or abandon it from CCS Cases before starting another graphical interview."
func _render_case_selection() -> void:
_show_selection()
_clear_children(case_list)
var available := compatible_cases(CCSService.cases)
if available.is_empty():
status_label.text = "No published CCS cases currently have a supported graphical interview."
return
status_label.text = "%d interactive %s available." % [available.size(), "case is" if available.size() == 1 else "cases are"]
retry_button.visible = false
for raw_case in available:
var case: Dictionary = raw_case
var button := Button.new()
button.name = "Case%s" % str(case.get("externalId", "")).to_pascal_case()
button.text = "%s\n%s · Difficulty %d · %d minutes" % [
str(case.get("title", "Untitled case")), str(case.get("system", "")),
int(case.get("difficulty", 0)), int(case.get("realTimeMinutes", 0))]
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.custom_minimum_size = Vector2(0, 76)
Style.style_answer_button(button)
button.pressed.connect(_start_case.bind(str(case.get("externalId", ""))))
case_list.add_child(button)
func _start_case(case_external_id: String) -> void:
if not pending_action.is_empty() or case_external_id.is_empty():
return
pending_action = "creating"
status_label.text = "Starting case..."
_set_case_buttons_disabled(true)
CCSService.create_attempt(case_external_id, "timed")
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.physical_exam_selected.connect(_submit_physical_exam)
active_scene.physical_exam_finish_requested.connect(_finish_physical_examination)
active_scene.order_search_requested.connect(_search_orders)
active_scene.orders_submit_requested.connect(_submit_orders)
active_scene.order_cancel_requested.connect(_cancel_order)
active_scene.time_advance_requested.connect(_advance_time)
active_scene.final_orders_submit_requested.connect(_submit_final_orders)
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(str(attempt.get("experienceMode", "management")))
active_scene.configure_presentation(
attempt.get("graphicalPresentation", {}),
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
active_scene.configure_examinations(attempt.get("availableExams", []), attempt.get("chart", []))
active_scene.configure_interview(node)
active_scene.configure_orders(attempt)
active_scene.set_choices_pending(not pending_action.is_empty())
func _submit_dialogue_choice(choice_id: String) -> void:
if not pending_action.is_empty() or choice_id.is_empty():
return
pending_action = "dialogue"
active_scene.set_choices_pending(true)
CCSService.send_dialogue_choice(choice_id)
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 = "enter_final_orders"
active_scene.set_physical_exam_pending("finishing_case")
CCSService.send_command("end_case")
func _search_orders(query: String) -> void:
CCSService.search_orders(query)
func _on_orders_changed() -> void:
if active_scene != null and active_scene.has_method("render_order_search"):
active_scene.render_order_search(CCSService.orders)
func _submit_orders(codes: Array) -> void:
if not pending_action.is_empty() or codes.is_empty():
return
pending_action = "orders"
active_scene.set_orders_pending(true, "Submitting orders...")
var commands: Array = []
for code in codes:
commands.append({"type": "order", "code": str(code), "minutes": 0})
CCSService.send_batch(commands)
func _cancel_order(code: String) -> void:
if not pending_action.is_empty() or code.is_empty():
return
pending_action = "cancel_order"
active_scene.set_orders_pending(true, "Cancelling order...")
CCSService.send_batch([{"type": "cancel_order", "code": code, "minutes": 0}])
func _advance_time(minutes: int) -> void:
if not pending_action.is_empty() or minutes <= 0:
return
pending_action = "advance"
active_scene.set_orders_pending(true, "Obtaining results...")
CCSService.send_command("advance", "", minutes)
func _submit_final_orders() -> void:
if not pending_action.is_empty():
return
pending_action = "submit_final_orders"
active_scene.set_orders_pending(true, "Submitting final orders...")
CCSService.send_command("exit_case")
func _render_debrief(attempt: Dictionary) -> void:
if active_scene == null:
_set_active_scene(DOCTOR_SCENE.instantiate(), "doctor_office")
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_presentation(
attempt.get("graphicalPresentation", {}),
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
active_scene.show_debrief(attempt.get("interviewDebrief", {}), attempt.get("score", {}))
current_view = "interview_debrief"
func _return_to_cases() -> void:
CCSService.attempt = {}
pending_action = ""
_render_case_selection()
func _repeat_interview() -> void:
var case_external_id := str(CCSService.attempt.get("caseExternalId", ""))
if case_external_id.is_empty() or not pending_action.is_empty():
return
pending_action = "creating"
CCSService.create_attempt(case_external_id, "timed")
func _is_retired_interview_revision(attempt: Dictionary, _node: Dictionary) -> bool:
if str(attempt.get("caseExternalId", "")) != FOCUSED_INTERVIEW_CASE_ID:
return false
var presentation: Dictionary = attempt.get("graphicalPresentation", {})
return int(presentation.get("interviewRevision", 0)) < FOCUSED_INTERVIEW_REVISION
func _restart_with_current_interview(attempt: Dictionary) -> void:
if pending_action == "refreshing_interview":
return
restart_case_external_id = str(attempt.get("caseExternalId", ""))
pending_action = "refreshing_interview"
_show_selection("Updating this case to the latest interactive examination...")
CCSService.abandon()
func _on_attempt_abandoned() -> void:
if restart_case_external_id.is_empty():
return
var case_external_id := restart_case_external_id
restart_case_external_id = ""
pending_action = "creating"
status_label.text = "Starting the updated interview..."
CCSService.create_attempt(case_external_id, "timed")
func _on_request_failed(message: String) -> void:
if not visible:
return
var failed_action := pending_action
pending_action = ""
if failed_action == "creating" and message.contains("409"):
status_label.text = "An active case was found. Loading it now..."
CCSService.load_active()
return
if failed_action == "refreshing_interview":
restart_case_external_id = ""
status_label.text = "The updated interview could not be started. %s" % message
retry_button.visible = true
return
if active_scene != null:
if failed_action == "physical_exam":
pending_exam_code = ""
active_scene.show_physical_exam_error(message)
else:
active_scene.set_choices_pending(false)
if active_scene.has_method("set_orders_pending"):
active_scene.set_orders_pending(false, message)
else:
status_label.text = message
retry_button.visible = failed_action == "loading"
_set_case_buttons_disabled(false)
func _retry_load() -> void:
on_show()
func _set_active_scene(scene: Control, view_name: String) -> void:
_clear_active_scene()
active_scene = scene
current_view = view_name
stage_root.add_child(active_scene)
active_scene.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
func _clear_active_scene() -> void:
if active_scene != null and is_instance_valid(active_scene):
if active_scene.get_parent() != null:
active_scene.get_parent().remove_child(active_scene)
active_scene.queue_free()
active_scene = null
if current_view == "doctor_office":
current_view = ""
func _set_case_buttons_disabled(disabled: bool) -> void:
for child in case_list.get_children():
if child is BaseButton:
child.disabled = disabled
func _clear_children(parent: Node) -> void:
for child in parent.get_children():
child.queue_free()
func _fit_stage() -> void:
if stage_root == null:
return
var available := size
if available.x <= 0.0 or available.y <= 0.0:
return
stage_root.scale = Vector2.ONE
stage_root.position = Vector2.ZERO
stage_root.size = available