Connect graphical CCS interviews to cases
This commit is contained in:
@@ -34,6 +34,7 @@ var queue: Array = []
|
||||
var text_scale := 1.0
|
||||
var highlights := {}
|
||||
var request_access_token := ""
|
||||
var launch_origin := "ccs"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -77,6 +78,10 @@ func send_command(type: String, code: String = "", minutes: int = 0) -> void:
|
||||
"minutes": minutes
|
||||
})
|
||||
|
||||
|
||||
func send_dialogue_choice(choice_id: String) -> void:
|
||||
send_command("dialogue_choice", choice_id)
|
||||
|
||||
func send_batch(commands: Array) -> void:
|
||||
if attempt.is_empty() or commands.is_empty():
|
||||
return
|
||||
@@ -272,6 +277,7 @@ func _escape_bbcode(value: String) -> String:
|
||||
|
||||
func _on_auth_changed() -> void:
|
||||
if not AuthState.is_logged_in:
|
||||
launch_origin = "ccs"
|
||||
highlights.clear()
|
||||
text_scale = 1.0
|
||||
attempt = {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
extends Control
|
||||
|
||||
signal dialogue_choice_selected(index: int, text: String)
|
||||
signal choice_selected(choice_id: String)
|
||||
signal interview_continue_requested
|
||||
|
||||
const CHOICE_NORMAL := preload("res://assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png")
|
||||
const CHOICE_HOVER := preload("res://assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png")
|
||||
@@ -22,11 +23,8 @@ var dialogue_tween: Tween
|
||||
func _ready() -> void:
|
||||
_update_clock()
|
||||
_schedule_patient_blink()
|
||||
show_patient_dialogue("Hello, doctor.")
|
||||
show_dialogue_choices([
|
||||
"What brings you in today?",
|
||||
"How are you feeling today?",
|
||||
])
|
||||
show_patient_dialogue("Loading patient interview...")
|
||||
hide_dialogue_choices()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
@@ -91,12 +89,28 @@ func _resize_dialogue_bubble() -> void:
|
||||
dialogue_bubble.size.y = target_height
|
||||
|
||||
|
||||
func show_dialogue_choices(options: Array[String]) -> void:
|
||||
func configure_interview(node: Dictionary) -> void:
|
||||
show_patient_dialogue(str(node.get("patientText", "")))
|
||||
if bool(node.get("isTerminal", false)):
|
||||
show_dialogue_choices([{"id": "__continue", "text": "Continue to Patient Management"}])
|
||||
else:
|
||||
show_dialogue_choices(node.get("choices", []))
|
||||
|
||||
|
||||
func set_choices_pending(pending: bool) -> void:
|
||||
for child in choice_container.get_children():
|
||||
if child is BaseButton:
|
||||
child.disabled = pending
|
||||
|
||||
|
||||
func show_dialogue_choices(options: Array) -> void:
|
||||
hide_dialogue_choices()
|
||||
var stack_height := options.size() * 97.0 + 8.0
|
||||
choice_container.offset_top = -4.0 - stack_height
|
||||
for index in options.size():
|
||||
var option_text := options[index]
|
||||
var option: Dictionary = options[index]
|
||||
var choice_id := str(option.get("id", ""))
|
||||
var option_text := str(option.get("text", ""))
|
||||
var button := TextureButton.new()
|
||||
button.custom_minimum_size = Vector2(420.0, 105.0)
|
||||
button.ignore_texture_size = true
|
||||
@@ -111,7 +125,7 @@ func show_dialogue_choices(options: Array[String]) -> void:
|
||||
button.mouse_exited.connect(_on_choice_hover.bind(button, false))
|
||||
button.button_down.connect(_on_choice_pressed_visual.bind(button, true))
|
||||
button.button_up.connect(_on_choice_pressed_visual.bind(button, false))
|
||||
button.pressed.connect(_on_dialogue_choice_pressed.bind(index, option_text))
|
||||
button.pressed.connect(_on_dialogue_choice_pressed.bind(choice_id))
|
||||
|
||||
var label := Label.new()
|
||||
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -156,6 +170,8 @@ func _on_choice_pressed_visual(button: TextureButton, pressed: bool) -> void:
|
||||
button.position.y += 2.0 if pressed else -2.0
|
||||
|
||||
|
||||
func _on_dialogue_choice_pressed(index: int, option_text: String) -> void:
|
||||
dialogue_choice_selected.emit(index, option_text)
|
||||
hide_dialogue_choices()
|
||||
func _on_dialogue_choice_pressed(choice_id: String) -> void:
|
||||
if choice_id == "__continue":
|
||||
interview_continue_requested.emit()
|
||||
else:
|
||||
choice_selected.emit(choice_id)
|
||||
|
||||
@@ -1,34 +1,232 @@
|
||||
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(1500.0, 800.0)
|
||||
|
||||
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 := ""
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
_show_doctor_office()
|
||||
CCSService.launch_origin = "ccs_cases_2"
|
||||
cases_loaded = false
|
||||
active_loaded = false
|
||||
pending_action = "loading"
|
||||
_show_selection("Loading interactive cases...")
|
||||
CCSService.load_cases()
|
||||
CCSService.load_active()
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
_clear_active_scene()
|
||||
|
||||
|
||||
func _show_doctor_office() -> void:
|
||||
_set_active_scene(DOCTOR_SCENE.instantiate(), "doctor_office")
|
||||
func compatible_cases(cases: Array) -> Array:
|
||||
return cases.filter(func(item: Variant) -> bool:
|
||||
return typeof(item) == TYPE_DICTIONARY and bool(item.get("interactiveInterviewAvailable", false)))
|
||||
|
||||
|
||||
func _on_cases_changed() -> void:
|
||||
cases_loaded = true
|
||||
_render_state()
|
||||
|
||||
|
||||
func _on_attempt_changed() -> void:
|
||||
active_loaded = true
|
||||
pending_action = ""
|
||||
_render_state()
|
||||
|
||||
|
||||
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", "")) 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", "")) != "active":
|
||||
_handoff_to_classic_player()
|
||||
return
|
||||
var interview: Dictionary = attempt.get("interview", {})
|
||||
if interview.is_empty():
|
||||
_handoff_to_classic_player()
|
||||
return
|
||||
var node: Dictionary = interview.get("currentNode", {})
|
||||
if node.is_empty():
|
||||
_handoff_to_classic_player()
|
||||
return
|
||||
if bool(attempt.get("openingCompleted", false)) and not bool(node.get("isTerminal", false)):
|
||||
_handoff_to_classic_player()
|
||||
return
|
||||
_show_doctor_office(node)
|
||||
|
||||
|
||||
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 = "Begin a timed patient interview, then continue into clinical management."
|
||||
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 _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 include an interactive 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) -> 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)
|
||||
selection_view.visible = false
|
||||
stage_root.visible = true
|
||||
active_scene.configure_interview(node)
|
||||
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 _handoff_to_classic_player() -> void:
|
||||
current_view = "handoff"
|
||||
if main != null:
|
||||
main.show_screen("ccs_player")
|
||||
|
||||
|
||||
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 active_scene != null:
|
||||
active_scene.set_choices_pending(false)
|
||||
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:
|
||||
@@ -45,7 +243,19 @@ func _clear_active_scene() -> void:
|
||||
active_scene.get_parent().remove_child(active_scene)
|
||||
active_scene.queue_free()
|
||||
active_scene = null
|
||||
current_view = ""
|
||||
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:
|
||||
|
||||
@@ -144,6 +144,7 @@ func _connect_services() -> void:
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
CCSService.launch_origin = "ccs"
|
||||
pending_action = ""
|
||||
pending_attempt_id = ""
|
||||
status_label.text = "Loading case history..."
|
||||
|
||||
@@ -820,7 +820,7 @@ func _confirm_abandon() -> void:
|
||||
func _on_abandoned() -> void:
|
||||
if leaving:
|
||||
leaving = false
|
||||
main.show_screen("ccs_hub")
|
||||
main.show_screen("ccs_cases_2" if CCSService.launch_origin == "ccs_cases_2" else "ccs_hub")
|
||||
|
||||
|
||||
func _render_chart() -> void:
|
||||
|
||||
@@ -36,7 +36,7 @@ func _ready() -> void:
|
||||
report_dialog = preload("res://scripts/ui/ccs_report_dialog.gd").new(); add_child(report_dialog)
|
||||
var row := HBoxContainer.new(); add_child(row)
|
||||
var cases := Button.new(); cases.text = "Another Case"; Style.style_primary_button(cases); cases.pressed.connect(_another_case); row.add_child(cases)
|
||||
var back := Button.new(); back.text = "Back to CCS Cases"; back.pressed.connect(func(): main.show_screen("ccs_hub")); row.add_child(back)
|
||||
var back := Button.new(); back.text = "Back to CCS Cases"; back.pressed.connect(_back_to_cases); row.add_child(back)
|
||||
status = Label.new(); Style.set_label(status, 14, Style.RED); row.add_child(status)
|
||||
CCSService.request_failed.connect(_request_failed)
|
||||
if not theme_service.theme_changed.is_connected(_on_theme_changed): theme_service.theme_changed.connect(_on_theme_changed)
|
||||
@@ -156,11 +156,18 @@ func _refresh_loaded_attempt() -> void:
|
||||
if str(CCSService.attempt.get("state", "")) == "completed": on_show(CCSService.attempt)
|
||||
|
||||
func _another_case() -> void:
|
||||
if CCSService.launch_origin == "ccs_cases_2":
|
||||
main.show_screen("ccs_cases_2")
|
||||
return
|
||||
if starting_case: return
|
||||
starting_case = true
|
||||
status.text = "Loading a random timed case..."
|
||||
CCSService.create_attempt("", "timed")
|
||||
|
||||
|
||||
func _back_to_cases() -> void:
|
||||
main.show_screen("ccs_cases_2" if CCSService.launch_origin == "ccs_cases_2" else "ccs_hub")
|
||||
|
||||
func _request_failed(message: String) -> void:
|
||||
if not visible or not starting_case: return
|
||||
starting_case = false
|
||||
|
||||
+4
-1
@@ -345,6 +345,7 @@ func _build_ui() -> void:
|
||||
if FeatureFlags.ccs_cases_2_enabled():
|
||||
var ccs_cases_2 := preload("res://scripts/ui/ccs_cases_2_screen.gd").new()
|
||||
ccs_cases_2.name = "CcsCases2"
|
||||
ccs_cases_2.main = self
|
||||
screens_container.add_child(ccs_cases_2)
|
||||
screens["ccs_cases_2"] = ccs_cases_2
|
||||
|
||||
@@ -501,7 +502,9 @@ func _on_navigation_pressed(key: String) -> void:
|
||||
"question_bank": show_screen("selection", "question_bank")
|
||||
"dynamic_questions": show_screen("dynamic_questions")
|
||||
"exams": show_screen("simulation_history")
|
||||
"ccs": show_screen("ccs_hub")
|
||||
"ccs":
|
||||
CCSService.launch_origin = "ccs"
|
||||
show_screen("ccs_hub")
|
||||
"ccs_cases_2": show_screen("ccs_cases_2")
|
||||
"flashcards": show_screen("flashcards_study")
|
||||
"performance": show_screen("profile")
|
||||
|
||||
@@ -18,7 +18,7 @@ static func key_for_screen(screen_name: String, payload: Variant = null) -> Stri
|
||||
elif screen_name == "ccs_cases_2":
|
||||
key = "ccs_cases_2"
|
||||
elif screen_name.begins_with("ccs_"):
|
||||
key = "ccs"
|
||||
key = "ccs_cases_2" if CCSService.launch_origin == "ccs_cases_2" else "ccs"
|
||||
elif screen_name.begins_with("flashcards_"):
|
||||
key = "flashcards"
|
||||
elif screen_name == "profile":
|
||||
|
||||
@@ -653,6 +653,7 @@ func _select_question_bank() -> void:
|
||||
|
||||
|
||||
func _select_ccs() -> void:
|
||||
CCSService.launch_origin = "ccs"
|
||||
pending_ccs_launch = true
|
||||
ccs_button.disabled = true
|
||||
ccs_status_label.text = "Selecting a timed case..."
|
||||
|
||||
Reference in New Issue
Block a user