1574 lines
60 KiB
GDScript
1574 lines
60 KiB
GDScript
extends Control
|
|
|
|
signal choice_selected(choice_id: String)
|
|
signal physical_exam_selected(exam_code: String)
|
|
signal physical_exam_finish_requested
|
|
signal order_search_requested(query: String)
|
|
signal orders_submit_requested(codes: Array)
|
|
signal order_cancel_requested(code: String)
|
|
signal time_advance_requested(minutes: int)
|
|
signal final_orders_submit_requested
|
|
signal debrief_return_requested
|
|
signal debrief_repeat_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")
|
|
const CHOICE_PRESSED := preload("res://assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png")
|
|
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 := 130.0
|
|
const DIALOGUE_BUBBLE_MAX_HEIGHT := 240.0
|
|
const DIALOGUE_BUBBLE_VERTICAL_CHROME := 95.0
|
|
|
|
@onready var hour_hand: Node2D = $ClockMount/HourHand
|
|
@onready var minute_hand: Node2D = $ClockMount/MinuteHand
|
|
@onready var second_hand: Node2D = $ClockMount/SecondHand
|
|
@onready var patient: TextureRect = $Patient
|
|
@onready var dialogue_bubble: Control = $DialogueBubble
|
|
@onready var dialogue_text: RichTextLabel = $DialogueBubble/Text
|
|
@onready var dialogue_continue_hint: Label = $DialogueBubble/ContinueHint
|
|
@onready var choice_area: Control = $DialogueChoiceArea
|
|
@onready var choice_container: HBoxContainer = $DialogueChoiceArea/DialogueChoices
|
|
@onready var choice_pager: HBoxContainer = $DialogueChoiceArea/ChoicePager
|
|
@onready var previous_page_button: Button = $DialogueChoiceArea/ChoicePager/Previous
|
|
@onready var page_label: Label = $DialogueChoiceArea/ChoicePager/Page
|
|
@onready var next_page_button: Button = $DialogueChoiceArea/ChoicePager/Next
|
|
@onready var computer_launcher: Button = $ComputerLauncher
|
|
@onready var examination_launcher: Button = $ExaminationLauncher
|
|
@onready var computer_overlay: Control = $ComputerOverlay
|
|
@onready var computer_close_button: Button = $ComputerOverlay/Close
|
|
@onready var vitals_launcher: Button = $ComputerOverlay/VitalsLauncher
|
|
@onready var vitals_window: PanelContainer = $ComputerOverlay/VitalsWindow
|
|
@onready var vitals_close_button: Button = $ComputerOverlay/VitalsWindow/Margin/Content/Header/Close
|
|
@onready var vitals_header: HBoxContainer = $ComputerOverlay/VitalsWindow/Margin/Content/Header
|
|
@onready var vitals_rows: GridContainer = $ComputerOverlay/VitalsWindow/Margin/Content/Rows
|
|
@onready var vitals_empty: Label = $ComputerOverlay/VitalsWindow/Margin/Content/Empty
|
|
@onready var medical_history_launcher: Button = $ComputerOverlay/MedicalHistoryLauncher
|
|
@onready var medical_history_window: PanelContainer = $ComputerOverlay/MedicalHistoryWindow
|
|
@onready var medical_history_header: HBoxContainer = $ComputerOverlay/MedicalHistoryWindow/Margin/Content/Header
|
|
@onready var medical_history_close_button: Button = $ComputerOverlay/MedicalHistoryWindow/Margin/Content/Header/Close
|
|
@onready var medical_history_text: Label = $ComputerOverlay/MedicalHistoryWindow/Margin/Content/Text
|
|
@onready var orders_launcher: Button = $ComputerOverlay/OrdersLauncher
|
|
|
|
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 physical_exam_close_button: Button
|
|
var physical_exam_result_button: Button
|
|
var physical_exam_terminal := false
|
|
var available_exams: Array = []
|
|
var attempt_chart: Array = []
|
|
var medical_history_section_title: Label
|
|
var medical_history_exam_text: RichTextLabel
|
|
var medical_history_exam_scroll: ScrollContainer
|
|
var medical_history_tab_button: Button
|
|
var physical_exam_tab_button: Button
|
|
var orders_window: PanelContainer
|
|
var orders_search: LineEdit
|
|
var orders_search_results: VBoxContainer
|
|
var orders_staged: VBoxContainer
|
|
var orders_active: VBoxContainer
|
|
var orders_results: RichTextLabel
|
|
var orders_tabs: TabContainer
|
|
var orders_confirm_button: Button
|
|
var orders_advance_button: Button
|
|
var orders_advance_option: OptionButton
|
|
var orders_submit_final_button: Button
|
|
var orders_close_button: Button
|
|
var orders_status: Label
|
|
var order_search_timer: Timer
|
|
var staged_order_codes: Array[String] = []
|
|
var order_names := {}
|
|
var order_options := {}
|
|
var final_orders_mode := false
|
|
var active_order_codes: Array = []
|
|
var current_order_location := ""
|
|
var current_order_minute := 0
|
|
var known_order_result_count := -1
|
|
var known_visit_completions: Array[String] = []
|
|
var visit_completion_tracking_ready := false
|
|
var patient_update_panel: PanelContainer
|
|
var patient_update_text: Label
|
|
var experience_mode := "management"
|
|
var pending_choices: Array = []
|
|
var available_choices: Array = []
|
|
var choice_page := 0
|
|
var dialogue_pages: Array[String] = []
|
|
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
|
|
var computer_available := true
|
|
var computer_drag_window: Control
|
|
var computer_drag_offset := Vector2.ZERO
|
|
var computer_window_z := 2
|
|
|
|
|
|
func _ready() -> void:
|
|
previous_page_button.pressed.connect(_change_choice_page.bind(-1))
|
|
next_page_button.pressed.connect(_change_choice_page.bind(1))
|
|
computer_launcher.pressed.connect(open_computer)
|
|
examination_launcher.pressed.connect(show_exam_selector)
|
|
computer_close_button.pressed.connect(close_computer)
|
|
vitals_launcher.pressed.connect(open_vitals)
|
|
vitals_close_button.pressed.connect(close_vitals)
|
|
medical_history_launcher.pressed.connect(open_medical_history)
|
|
medical_history_close_button.pressed.connect(close_medical_history)
|
|
orders_launcher.pressed.connect(open_orders)
|
|
vitals_header.gui_input.connect(_on_computer_window_header_input.bind(vitals_window, vitals_close_button))
|
|
medical_history_header.gui_input.connect(_on_computer_window_header_input.bind(
|
|
medical_history_window, medical_history_close_button))
|
|
vitals_window.gui_input.connect(_on_computer_window_input.bind(vitals_window))
|
|
medical_history_window.gui_input.connect(_on_computer_window_input.bind(medical_history_window))
|
|
resized.connect(_resize_computer_windows)
|
|
_style_vitals_window()
|
|
_style_computer_window(medical_history_window)
|
|
_style_desktop_shortcut(vitals_launcher)
|
|
_style_desktop_shortcut(medical_history_launcher)
|
|
_style_desktop_shortcut(orders_launcher)
|
|
_build_medical_history_tabs()
|
|
_build_orders_window()
|
|
_build_patient_update()
|
|
_resize_computer_windows()
|
|
_update_clock()
|
|
_schedule_patient_blink()
|
|
hide_patient_dialogue()
|
|
hide_dialogue_choices()
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
pass
|
|
|
|
|
|
func _update_clock() -> void:
|
|
var hour_value := floori(clock_seconds / 3600.0)
|
|
var minute_value := floori(fmod(clock_seconds, 3600.0) / 60.0)
|
|
var second_value := fmod(clock_seconds, 60.0)
|
|
var second := float(second_value)
|
|
var minute := float(minute_value) + second / 60.0
|
|
var hour := float(hour_value % 12) + minute / 60.0
|
|
hour_hand.rotation = TAU * hour / 12.0
|
|
minute_hand.rotation = TAU * minute / 60.0
|
|
second_hand.rotation = TAU * second / 60.0
|
|
|
|
|
|
func configure_presentation(
|
|
presentation: Dictionary, simulated_start: String, simulated_minute: int,
|
|
vitals: Dictionary = {}, medical_history: String = "", chart: Array = []) -> void:
|
|
if str(presentation.get("patientAssetKey", "")) != "patient_44_white_male":
|
|
push_warning("Unsupported CCS patient asset key; using the office default.")
|
|
var date_time := Time.get_datetime_dict_from_datetime_string(simulated_start, false)
|
|
if not date_time.is_empty():
|
|
clock_seconds = float(int(date_time.get("hour", 9)) * 3600 + int(date_time.get("minute", 0)) * 60)
|
|
clock_seconds += float(simulated_minute * 60)
|
|
_update_clock()
|
|
_render_vitals(vitals)
|
|
medical_history_text.text = medical_history.strip_edges() \
|
|
if not medical_history.strip_edges().is_empty() \
|
|
else "No past medical history is available."
|
|
attempt_chart = chart.duplicate(true)
|
|
_render_physical_exam_history()
|
|
|
|
|
|
func configure_experience_mode(value: String) -> void:
|
|
experience_mode = value
|
|
|
|
|
|
func _schedule_patient_blink() -> void:
|
|
blink_tween = create_tween()
|
|
blink_tween.tween_interval(randf_range(2.8, 5.2))
|
|
blink_tween.tween_method(_set_patient_blink, 0.0, 1.0, 0.075).set_trans(
|
|
Tween.TRANS_SINE
|
|
).set_ease(Tween.EASE_IN)
|
|
blink_tween.tween_interval(0.045)
|
|
blink_tween.tween_method(_set_patient_blink, 1.0, 0.0, 0.10).set_trans(
|
|
Tween.TRANS_SINE
|
|
).set_ease(Tween.EASE_OUT)
|
|
blink_tween.tween_callback(_schedule_patient_blink)
|
|
|
|
|
|
func _set_patient_blink(amount: float) -> void:
|
|
patient.material.set_shader_parameter("blink_amount", amount)
|
|
|
|
|
|
func show_patient_dialogue(text: String, characters_per_second: float = 35.0) -> void:
|
|
if dialogue_tween and dialogue_tween.is_valid():
|
|
dialogue_tween.kill()
|
|
dialogue_pages = _split_dialogue_pages(text)
|
|
dialogue_page_index = 0
|
|
dialogue_characters_per_second = characters_per_second
|
|
_show_dialogue_page()
|
|
|
|
|
|
func _show_dialogue_page() -> void:
|
|
if dialogue_tween and dialogue_tween.is_valid():
|
|
dialogue_tween.kill()
|
|
if dialogue_pages.is_empty():
|
|
complete_response_transition()
|
|
return
|
|
dialogue_text.text = dialogue_pages[dialogue_page_index]
|
|
dialogue_text.visible_ratio = 0.0
|
|
dialogue_reveal_complete = false
|
|
dialogue_continue_hint.text = "Click to continue"
|
|
dialogue_continue_hint.visible = true
|
|
dialogue_bubble.visible = true
|
|
dialogue_bubble.grab_focus()
|
|
dialogue_layout_generation += 1
|
|
call_deferred("_prepare_dialogue_page", dialogue_layout_generation)
|
|
|
|
|
|
func _prepare_dialogue_page(generation: int) -> void:
|
|
if generation != dialogue_layout_generation or not dialogue_bubble.visible \
|
|
or dialogue_reveal_complete:
|
|
return
|
|
var measured_height := dialogue_text.get_content_height() + DIALOGUE_BUBBLE_VERTICAL_CHROME
|
|
dialogue_bubble.size.y = clampf(
|
|
measured_height, DIALOGUE_BUBBLE_MIN_HEIGHT, DIALOGUE_BUBBLE_MAX_HEIGHT)
|
|
var reveal_duration := float(dialogue_text.text.length()) / maxf(dialogue_characters_per_second, 1.0)
|
|
dialogue_tween = create_tween()
|
|
dialogue_tween.tween_property(dialogue_text, "visible_ratio", 1.0, reveal_duration)
|
|
dialogue_tween.finished.connect(_on_dialogue_reveal_finished)
|
|
|
|
|
|
func hide_patient_dialogue() -> void:
|
|
if dialogue_tween and dialogue_tween.is_valid():
|
|
dialogue_tween.kill()
|
|
dialogue_layout_generation += 1
|
|
dialogue_bubble.visible = false
|
|
dialogue_continue_hint.visible = false
|
|
dialogue_pages = []
|
|
dialogue_page_index = 0
|
|
dialogue_reveal_complete = false
|
|
pending_choices = []
|
|
|
|
|
|
func is_patient_dialogue_visible() -> bool:
|
|
return dialogue_bubble.visible
|
|
|
|
|
|
func configure_interview(node: Dictionary) -> void:
|
|
computer_available = true
|
|
computer_launcher.visible = not computer_overlay.visible
|
|
if debrief_panel != null and is_instance_valid(debrief_panel):
|
|
debrief_panel.queue_free()
|
|
debrief_panel = null
|
|
var next_choices: Array
|
|
if bool(node.get("isTerminal", false)):
|
|
next_choices = [{
|
|
"id": "__continue",
|
|
"text": "Review Examinations" if experience_mode == "graphical_interview" else "Continue to Patient Management"
|
|
}]
|
|
else:
|
|
next_choices = node.get("choices", [])
|
|
hide_dialogue_choices()
|
|
if bool(node.get("suppressPatientText", false)):
|
|
hide_patient_dialogue()
|
|
show_dialogue_choices(next_choices)
|
|
return
|
|
pending_choices = next_choices
|
|
show_patient_dialogue(str(node.get("patientText", "")))
|
|
|
|
|
|
func complete_dialogue_reveal() -> void:
|
|
if dialogue_reveal_complete:
|
|
return
|
|
if dialogue_tween and dialogue_tween.is_valid():
|
|
dialogue_tween.kill()
|
|
dialogue_layout_generation += 1
|
|
dialogue_text.visible_ratio = 1.0
|
|
_on_dialogue_reveal_finished()
|
|
|
|
|
|
func _on_dialogue_reveal_finished() -> void:
|
|
dialogue_tween = null
|
|
dialogue_reveal_complete = true
|
|
dialogue_continue_hint.text = "Click to continue"
|
|
dialogue_continue_hint.visible = true
|
|
|
|
|
|
func advance_dialogue() -> void:
|
|
if not dialogue_bubble.visible:
|
|
return
|
|
if not dialogue_reveal_complete:
|
|
complete_dialogue_reveal()
|
|
return
|
|
if dialogue_page_index < dialogue_pages.size() - 1:
|
|
dialogue_page_index += 1
|
|
_show_dialogue_page()
|
|
return
|
|
complete_response_transition()
|
|
|
|
|
|
func complete_response_transition() -> void:
|
|
var choices := pending_choices
|
|
pending_choices = []
|
|
dialogue_bubble.visible = false
|
|
dialogue_continue_hint.visible = false
|
|
dialogue_pages = []
|
|
dialogue_page_index = 0
|
|
dialogue_reveal_complete = false
|
|
show_dialogue_choices(choices)
|
|
_update_examination_launcher()
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if patient_update_panel != null and patient_update_panel.visible:
|
|
return
|
|
if computer_overlay.visible or (physical_exam_panel != null and physical_exam_panel.visible):
|
|
return
|
|
if _event_targets_computer_launcher(event):
|
|
return
|
|
if not dialogue_input_enabled or not dialogue_bubble.visible:
|
|
return
|
|
var should_advance := false
|
|
if event is InputEventMouseButton:
|
|
should_advance = event.button_index == MOUSE_BUTTON_LEFT and event.pressed \
|
|
and get_global_rect().has_point(event.position)
|
|
elif event is InputEventScreenTouch:
|
|
should_advance = event.pressed and get_global_rect().has_point(event.position)
|
|
elif event is InputEventKey:
|
|
should_advance = event.pressed and not event.echo \
|
|
and event.keycode in [KEY_ENTER, KEY_KP_ENTER, KEY_SPACE]
|
|
if not should_advance:
|
|
return
|
|
advance_dialogue()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func set_dialogue_input_enabled(enabled: bool) -> void:
|
|
dialogue_input_enabled = enabled
|
|
|
|
|
|
func open_computer() -> void:
|
|
if not computer_available or computer_overlay.visible:
|
|
return
|
|
computer_overlay.visible = true
|
|
computer_launcher.visible = false
|
|
if dialogue_tween and dialogue_tween.is_valid():
|
|
dialogue_tween.pause()
|
|
computer_close_button.grab_focus()
|
|
|
|
|
|
func close_computer() -> bool:
|
|
if not computer_overlay.visible:
|
|
return false
|
|
close_vitals()
|
|
close_medical_history()
|
|
close_orders()
|
|
computer_overlay.visible = false
|
|
computer_launcher.visible = computer_available
|
|
if dialogue_tween and dialogue_tween.is_valid():
|
|
dialogue_tween.play()
|
|
return true
|
|
|
|
|
|
func is_computer_open() -> bool:
|
|
return computer_overlay.visible
|
|
|
|
|
|
func close_transient_overlay() -> bool:
|
|
if patient_update_panel != null and patient_update_panel.visible:
|
|
patient_update_panel.visible = false
|
|
return true
|
|
if physical_exam_panel != null and physical_exam_panel.visible:
|
|
if physical_exam_result_button.visible:
|
|
_close_exam_result()
|
|
elif not physical_exam_terminal:
|
|
close_exam_overlay()
|
|
else:
|
|
return false
|
|
return true
|
|
var top_window := _topmost_computer_window()
|
|
if top_window == vitals_window:
|
|
return close_vitals()
|
|
if top_window == medical_history_window:
|
|
return close_medical_history()
|
|
if top_window == orders_window:
|
|
return close_orders()
|
|
return close_computer()
|
|
|
|
|
|
func open_vitals() -> void:
|
|
if not computer_overlay.visible:
|
|
return
|
|
vitals_window.visible = true
|
|
_bring_computer_window_to_front(vitals_window)
|
|
vitals_close_button.grab_focus()
|
|
|
|
|
|
func close_vitals() -> bool:
|
|
if not vitals_window.visible:
|
|
return false
|
|
if computer_drag_window == vitals_window:
|
|
computer_drag_window = null
|
|
vitals_window.visible = false
|
|
return true
|
|
|
|
|
|
func is_vitals_open() -> bool:
|
|
return vitals_window.visible
|
|
|
|
|
|
func open_medical_history() -> void:
|
|
if not computer_overlay.visible:
|
|
return
|
|
medical_history_window.visible = true
|
|
_bring_computer_window_to_front(medical_history_window)
|
|
medical_history_close_button.grab_focus()
|
|
|
|
|
|
func close_medical_history() -> bool:
|
|
if not medical_history_window.visible:
|
|
return false
|
|
if computer_drag_window == medical_history_window:
|
|
computer_drag_window = null
|
|
medical_history_window.visible = false
|
|
return true
|
|
|
|
|
|
func is_medical_history_open() -> bool:
|
|
return medical_history_window.visible
|
|
|
|
|
|
func _build_orders_window() -> void:
|
|
orders_window = PanelContainer.new()
|
|
orders_window.name = "OrdersWindow"
|
|
orders_window.visible = false
|
|
orders_window.z_index = 2
|
|
orders_window.size = Vector2(1040.0, 700.0)
|
|
orders_window.position = Vector2(280.0, 100.0)
|
|
_style_computer_window(orders_window)
|
|
computer_overlay.add_child(orders_window)
|
|
var margin := MarginContainer.new()
|
|
margin.add_theme_constant_override("margin_left", 28)
|
|
margin.add_theme_constant_override("margin_top", 22)
|
|
margin.add_theme_constant_override("margin_right", 28)
|
|
margin.add_theme_constant_override("margin_bottom", 24)
|
|
orders_window.add_child(margin)
|
|
var content := VBoxContainer.new()
|
|
content.add_theme_constant_override("separation", 12)
|
|
margin.add_child(content)
|
|
var header := HBoxContainer.new()
|
|
header.mouse_default_cursor_shape = Control.CURSOR_MOVE
|
|
content.add_child(header)
|
|
var title := Label.new()
|
|
title.text = "Orders"
|
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
title.add_theme_font_size_override("font_size", 30)
|
|
header.add_child(title)
|
|
orders_close_button = Button.new()
|
|
orders_close_button.text = "Close"
|
|
orders_close_button.pressed.connect(close_orders)
|
|
header.add_child(orders_close_button)
|
|
header.gui_input.connect(_on_computer_window_header_input.bind(orders_window, orders_close_button))
|
|
orders_window.gui_input.connect(_on_computer_window_input.bind(orders_window))
|
|
orders_status = Label.new()
|
|
orders_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
content.add_child(orders_status)
|
|
orders_tabs = TabContainer.new()
|
|
orders_tabs.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
content.add_child(orders_tabs)
|
|
_build_order_search_tab()
|
|
_build_active_orders_tab()
|
|
_build_order_results_tab()
|
|
var actions := HBoxContainer.new()
|
|
actions.alignment = BoxContainer.ALIGNMENT_END
|
|
content.add_child(actions)
|
|
orders_advance_option = OptionButton.new()
|
|
for item in [["10 minutes", 10], ["1 hour", 60], ["4 hours", 240], ["1 day", 1440]]:
|
|
orders_advance_option.add_item(item[0])
|
|
orders_advance_option.set_item_metadata(orders_advance_option.item_count - 1, item[1])
|
|
actions.add_child(orders_advance_option)
|
|
orders_advance_button = Button.new()
|
|
orders_advance_button.text = "Obtain Results"
|
|
orders_advance_button.pressed.connect(func(): time_advance_requested.emit(
|
|
int(orders_advance_option.get_item_metadata(orders_advance_option.selected))))
|
|
actions.add_child(orders_advance_button)
|
|
orders_submit_final_button = Button.new()
|
|
orders_submit_final_button.text = "Submit Final Orders"
|
|
orders_submit_final_button.visible = false
|
|
orders_submit_final_button.pressed.connect(func(): final_orders_submit_requested.emit())
|
|
actions.add_child(orders_submit_final_button)
|
|
|
|
|
|
func _build_order_search_tab() -> void:
|
|
var page := VBoxContainer.new()
|
|
page.name = "Search"
|
|
page.add_theme_constant_override("separation", 8)
|
|
orders_tabs.add_child(page)
|
|
var row := HBoxContainer.new()
|
|
page.add_child(row)
|
|
orders_search = LineEdit.new()
|
|
orders_search.placeholder_text = "Search tests, medications, treatments, and procedures"
|
|
orders_search.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
orders_search.text_submitted.connect(func(_value): _request_order_search())
|
|
orders_search.text_changed.connect(_queue_live_order_search)
|
|
row.add_child(orders_search)
|
|
order_search_timer = Timer.new()
|
|
order_search_timer.one_shot = true
|
|
order_search_timer.wait_time = 0.35
|
|
order_search_timer.timeout.connect(_request_order_search)
|
|
add_child(order_search_timer)
|
|
var search_button := Button.new()
|
|
search_button.text = "Search"
|
|
search_button.pressed.connect(_request_order_search)
|
|
row.add_child(search_button)
|
|
var search_scroll := ScrollContainer.new()
|
|
search_scroll.custom_minimum_size.y = 170.0
|
|
search_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
page.add_child(search_scroll)
|
|
orders_search_results = VBoxContainer.new()
|
|
orders_search_results.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
search_scroll.add_child(orders_search_results)
|
|
page.add_child(Label.new())
|
|
page.get_child(page.get_child_count() - 1).text = "Orders awaiting confirmation"
|
|
orders_staged = VBoxContainer.new()
|
|
page.add_child(orders_staged)
|
|
orders_confirm_button = Button.new()
|
|
orders_confirm_button.text = "Confirm Orders"
|
|
orders_confirm_button.disabled = true
|
|
orders_confirm_button.pressed.connect(_confirm_staged_orders)
|
|
page.add_child(orders_confirm_button)
|
|
|
|
|
|
func _build_active_orders_tab() -> void:
|
|
var page := VBoxContainer.new()
|
|
page.name = "Active Orders"
|
|
orders_tabs.add_child(page)
|
|
var scroll := ScrollContainer.new()
|
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
page.add_child(scroll)
|
|
orders_active = VBoxContainer.new()
|
|
orders_active.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
scroll.add_child(orders_active)
|
|
|
|
|
|
func _build_order_results_tab() -> void:
|
|
var page := VBoxContainer.new()
|
|
page.name = "Results"
|
|
orders_tabs.add_child(page)
|
|
orders_results = RichTextLabel.new()
|
|
orders_results.bbcode_enabled = true
|
|
orders_results.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
page.add_child(orders_results)
|
|
|
|
|
|
func open_orders() -> void:
|
|
if not computer_overlay.visible:
|
|
return
|
|
orders_window.visible = true
|
|
orders_launcher.get_node("Label").text = "Orders"
|
|
_bring_computer_window_to_front(orders_window)
|
|
orders_search.grab_focus()
|
|
|
|
|
|
func close_orders() -> bool:
|
|
if orders_window == null or not orders_window.visible:
|
|
return false
|
|
orders_window.visible = false
|
|
return true
|
|
|
|
|
|
func _request_order_search() -> void:
|
|
var query := orders_search.text.strip_edges()
|
|
if query.length() < 2:
|
|
orders_status.text = "Enter at least two characters to search."
|
|
return
|
|
orders_status.text = "Searching..."
|
|
order_search_requested.emit(query)
|
|
|
|
|
|
func _queue_live_order_search(query: String) -> void:
|
|
order_search_timer.stop()
|
|
if query.strip_edges().length() < 2:
|
|
_clear_dynamic_children(orders_search_results)
|
|
orders_status.text = ""
|
|
return
|
|
order_search_timer.start()
|
|
|
|
|
|
func render_order_search(rows: Array) -> void:
|
|
_clear_dynamic_children(orders_search_results)
|
|
var result_count := 0
|
|
for raw_order in rows:
|
|
var order: Dictionary = raw_order
|
|
result_count += 1
|
|
var code := str(order.get("code", ""))
|
|
order_names[code] = str(order.get("canonicalName", code.capitalize()))
|
|
var option: Dictionary = order_options.get(code, {})
|
|
var configured := not option.is_empty()
|
|
var button := Button.new()
|
|
var detail := str(order.get("category", "")).capitalize()
|
|
if configured:
|
|
var location := _humanize_order_location(str(option.get("performanceLocation", current_order_location)))
|
|
var delay := int(option.get("turnaroundMinutes", 0))
|
|
detail += " - %s%s" % [location, " - %d min" % delay if delay > 0 else ""]
|
|
else:
|
|
detail += " - Unavailable in this case"
|
|
button.text = "%s\n%s" % [order_names[code], detail]
|
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
button.disabled = not configured or code in staged_order_codes or code in active_order_codes
|
|
button.tooltip_text = "This order is not configured for this case." if not configured else ""
|
|
button.pressed.connect(_stage_order.bind(code))
|
|
orders_search_results.add_child(button)
|
|
orders_status.text = "" if result_count > 0 else "No matching orders found."
|
|
|
|
|
|
func _stage_order(code: String) -> void:
|
|
if code.is_empty() or code in staged_order_codes:
|
|
return
|
|
staged_order_codes.append(code)
|
|
_render_staged_orders()
|
|
|
|
|
|
func _render_staged_orders() -> void:
|
|
_clear_dynamic_children(orders_staged)
|
|
for code in staged_order_codes:
|
|
var row := HBoxContainer.new()
|
|
orders_staged.add_child(row)
|
|
var label := Label.new()
|
|
var option: Dictionary = order_options.get(code, {})
|
|
var location := _humanize_order_location(str(option.get("performanceLocation", current_order_location)))
|
|
label.text = "%s - %s" % [str(order_names.get(code, code.capitalize())), location]
|
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
row.add_child(label)
|
|
var remove := Button.new()
|
|
remove.text = "Remove"
|
|
remove.pressed.connect(_remove_staged_order.bind(code))
|
|
row.add_child(remove)
|
|
orders_confirm_button.disabled = staged_order_codes.is_empty()
|
|
|
|
|
|
func _remove_staged_order(code: String) -> void:
|
|
staged_order_codes.erase(code)
|
|
_render_staged_orders()
|
|
|
|
|
|
func _confirm_staged_orders() -> void:
|
|
if staged_order_codes.is_empty():
|
|
return
|
|
orders_submit_requested.emit(staged_order_codes.duplicate())
|
|
|
|
|
|
func configure_orders(attempt: Dictionary) -> void:
|
|
if orders_window == null:
|
|
return
|
|
current_order_location = str(attempt.get("location", ""))
|
|
current_order_minute = int(attempt.get("simulatedMinute", 0))
|
|
order_options.clear()
|
|
for raw_option in attempt.get("orderOptions", []):
|
|
var option: Dictionary = raw_option
|
|
order_options[str(option.get("code", ""))] = option
|
|
active_order_codes = attempt.get("activeOrderCodes", []).duplicate()
|
|
for code in attempt.get("activeOrderCodes", []):
|
|
staged_order_codes.erase(str(code))
|
|
_render_staged_orders()
|
|
_render_active_orders(attempt.get("activeOrders", []))
|
|
_render_order_results(attempt.get("chart", []))
|
|
_track_patient_visit_completions(attempt.get("chart", []))
|
|
var result_count := _order_result_count(attempt.get("chart", []))
|
|
if known_order_result_count >= 0 and result_count > known_order_result_count and not orders_window.visible:
|
|
orders_launcher.get_node("Label").text = "Orders •"
|
|
known_order_result_count = result_count
|
|
final_orders_mode = str(attempt.get("state", "")) == "final_orders"
|
|
orders_advance_button.disabled = false
|
|
orders_submit_final_button.disabled = false
|
|
orders_advance_button.visible = not final_orders_mode
|
|
orders_advance_option.visible = not final_orders_mode
|
|
orders_submit_final_button.visible = final_orders_mode
|
|
orders_close_button.visible = true
|
|
if final_orders_mode:
|
|
orders_status.text = "FINAL STEP — Review your active orders. Add or cancel anything necessary, then select Submit Final Orders. Pending results do not need to return first."
|
|
open_computer()
|
|
open_orders()
|
|
elif orders_status.text.begins_with("Submitting") or orders_status.text.begins_with("Cancelling") \
|
|
or orders_status.text.begins_with("Obtaining"):
|
|
orders_status.text = ""
|
|
|
|
|
|
func show_order_action_complete(action: String) -> void:
|
|
match action:
|
|
"orders":
|
|
orders_tabs.current_tab = 1
|
|
orders_status.text = "Orders confirmed and automatically scheduled. Use Obtain Results to advance time."
|
|
"cancel_order":
|
|
orders_tabs.current_tab = 1
|
|
orders_status.text = "Order cancelled."
|
|
"advance":
|
|
orders_tabs.current_tab = 2
|
|
orders_status.text = "Simulated time advanced. Newly available results are shown here."
|
|
|
|
|
|
func _render_active_orders(rows: Array) -> void:
|
|
_clear_dynamic_children(orders_active)
|
|
if rows.is_empty():
|
|
var empty := Label.new()
|
|
empty.text = "No active orders."
|
|
orders_active.add_child(empty)
|
|
return
|
|
for raw_order in rows:
|
|
var order: Dictionary = raw_order
|
|
var row := HBoxContainer.new()
|
|
orders_active.add_child(row)
|
|
var label := Label.new()
|
|
var status := str(order.get("status", "active")).capitalize()
|
|
var location := _humanize_order_location(str(order.get("performanceLocation", current_order_location)))
|
|
var due_text := ""
|
|
if order.get("dueMinute", null) != null:
|
|
due_text = " - due in %d min" % maxi(0, int(order.get("dueMinute", 0)) - current_order_minute)
|
|
label.text = "%s - %s at %s%s" % [str(order.get("name", "Order")), status, location, due_text]
|
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
row.add_child(label)
|
|
var cancel := Button.new()
|
|
cancel.text = "Cancel"
|
|
cancel.pressed.connect(_emit_cancel_order.bind(str(order.get("code", ""))))
|
|
row.add_child(cancel)
|
|
|
|
|
|
func _render_order_results(chart: Array) -> void:
|
|
var parts: Array[String] = []
|
|
for raw_entry in chart:
|
|
var entry: Dictionary = raw_entry
|
|
if str(entry.get("category", "")) in ["lab", "imaging", "other_test", "treatment"]:
|
|
parts.append("[b]%s[/b]\n%s" % [str(entry.get("title", "Result")), str(entry.get("text", ""))])
|
|
orders_results.text = "\n\n".join(parts) if not parts.is_empty() else "No results are available."
|
|
|
|
|
|
func _order_result_count(chart: Array) -> int:
|
|
var count := 0
|
|
for raw_entry in chart:
|
|
if str(raw_entry.get("category", "")) in ["lab", "imaging", "other_test", "treatment"]:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _humanize_order_location(value: String) -> String:
|
|
if value.is_empty():
|
|
return "Current location"
|
|
return value.replace("_", " ").capitalize()
|
|
|
|
|
|
func _build_patient_update() -> void:
|
|
patient_update_panel = PanelContainer.new()
|
|
patient_update_panel.name = "PatientProcedureUpdate"
|
|
patient_update_panel.visible = false
|
|
patient_update_panel.z_index = 100
|
|
patient_update_panel.position = Vector2(400, 300)
|
|
patient_update_panel.custom_minimum_size = Vector2(800, 0)
|
|
add_child(patient_update_panel)
|
|
var margin := MarginContainer.new()
|
|
margin.add_theme_constant_override("margin_left", 24)
|
|
margin.add_theme_constant_override("margin_right", 24)
|
|
margin.add_theme_constant_override("margin_top", 18)
|
|
margin.add_theme_constant_override("margin_bottom", 18)
|
|
patient_update_panel.add_child(margin)
|
|
var content := VBoxContainer.new()
|
|
content.add_theme_constant_override("separation", 12)
|
|
margin.add_child(content)
|
|
var heading := Label.new()
|
|
heading.text = "Patient Update"
|
|
heading.add_theme_font_size_override("font_size", 24)
|
|
content.add_child(heading)
|
|
patient_update_text = Label.new()
|
|
patient_update_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
patient_update_text.add_theme_font_size_override("font_size", 18)
|
|
content.add_child(patient_update_text)
|
|
var dismiss := Button.new()
|
|
dismiss.text = "Continue"
|
|
dismiss.custom_minimum_size = Vector2(140, 44)
|
|
dismiss.size_flags_horizontal = Control.SIZE_SHRINK_END
|
|
dismiss.pressed.connect(func() -> void: patient_update_panel.visible = false)
|
|
content.add_child(dismiss)
|
|
|
|
|
|
func _track_patient_visit_completions(chart: Array) -> void:
|
|
var current: Array[String] = []
|
|
var new_messages: Array[String] = []
|
|
for raw_entry in chart:
|
|
var entry: Dictionary = raw_entry
|
|
var code := str(entry.get("code", ""))
|
|
if str(entry.get("category", "")) != "progress" or not code.ends_with("_completed"):
|
|
continue
|
|
var key := "%s:%d" % [code, int(entry.get("simulatedMinute", 0))]
|
|
current.append(key)
|
|
if visit_completion_tracking_ready and key not in known_visit_completions:
|
|
new_messages.append(str(entry.get("text", "The patient completed a scheduled procedure and returned.")))
|
|
known_visit_completions = current
|
|
if not visit_completion_tracking_ready:
|
|
visit_completion_tracking_ready = true
|
|
return
|
|
if not new_messages.is_empty():
|
|
patient_update_text.text = "\n".join(new_messages) + "\nNew results are available in the computer."
|
|
patient_update_panel.visible = true
|
|
|
|
|
|
func set_orders_pending(pending: bool, message: String = "") -> void:
|
|
orders_confirm_button.disabled = pending or staged_order_codes.is_empty()
|
|
orders_advance_button.disabled = pending
|
|
orders_submit_final_button.disabled = pending
|
|
orders_status.text = message
|
|
|
|
|
|
func _emit_cancel_order(code: String) -> void:
|
|
order_cancel_requested.emit(code)
|
|
|
|
|
|
func _clear_dynamic_children(parent: Node) -> void:
|
|
for child in parent.get_children():
|
|
child.queue_free()
|
|
|
|
|
|
func _build_medical_history_tabs() -> void:
|
|
var content := medical_history_text.get_parent() as VBoxContainer
|
|
medical_history_section_title = content.get_node("SectionTitle") as Label
|
|
var tabs := HBoxContainer.new()
|
|
tabs.add_theme_constant_override("separation", 8)
|
|
content.add_child(tabs)
|
|
content.move_child(tabs, medical_history_section_title.get_index())
|
|
medical_history_tab_button = Button.new()
|
|
medical_history_tab_button.text = "Medical History"
|
|
medical_history_tab_button.pressed.connect(_show_chart_tab.bind(false))
|
|
tabs.add_child(medical_history_tab_button)
|
|
physical_exam_tab_button = Button.new()
|
|
physical_exam_tab_button.text = "Physical Exam"
|
|
physical_exam_tab_button.pressed.connect(_show_chart_tab.bind(true))
|
|
tabs.add_child(physical_exam_tab_button)
|
|
medical_history_exam_scroll = ScrollContainer.new()
|
|
medical_history_exam_scroll.custom_minimum_size = Vector2(0.0, 120.0)
|
|
medical_history_exam_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
medical_history_exam_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
content.add_child(medical_history_exam_scroll)
|
|
medical_history_exam_text = RichTextLabel.new()
|
|
medical_history_exam_text.bbcode_enabled = true
|
|
medical_history_exam_text.fit_content = true
|
|
medical_history_exam_text.custom_minimum_size = Vector2(560.0, 120.0)
|
|
medical_history_exam_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
medical_history_exam_text.add_theme_font_size_override("normal_font_size", 18)
|
|
medical_history_exam_scroll.add_child(medical_history_exam_text)
|
|
_show_chart_tab(false)
|
|
|
|
|
|
func _show_chart_tab(show_exams: bool) -> void:
|
|
medical_history_section_title.visible = not show_exams
|
|
medical_history_text.visible = not show_exams
|
|
medical_history_exam_scroll.visible = show_exams
|
|
medical_history_tab_button.disabled = not show_exams
|
|
physical_exam_tab_button.disabled = show_exams
|
|
_resize_computer_windows()
|
|
|
|
|
|
func _render_physical_exam_history() -> void:
|
|
if medical_history_exam_text == null:
|
|
return
|
|
var exam_codes := {}
|
|
for raw_exam in available_exams:
|
|
exam_codes[str(raw_exam.get("code", ""))] = true
|
|
var findings: Array[String] = []
|
|
for raw_entry in attempt_chart:
|
|
var entry: Dictionary = raw_entry
|
|
if exam_codes.has(str(entry.get("code", ""))):
|
|
findings.append("[b]%s[/b]\n%s" % [str(entry.get("title", "Examination")), str(entry.get("text", ""))])
|
|
medical_history_exam_text.text = "\n\n".join(findings) if not findings.is_empty() \
|
|
else "No physical examinations have been completed."
|
|
|
|
|
|
func _update_examination_launcher() -> void:
|
|
if examination_launcher == null:
|
|
return
|
|
examination_launcher.visible = choice_container.visible and not computer_overlay.visible \
|
|
and (physical_exam_panel == null or not physical_exam_panel.visible)
|
|
examination_launcher.disabled = available_exams.is_empty()
|
|
|
|
|
|
func _render_vitals(vitals: Dictionary) -> void:
|
|
for child in vitals_rows.get_children():
|
|
vitals_rows.remove_child(child)
|
|
child.queue_free()
|
|
var rows := _build_vital_rows(vitals)
|
|
for row in rows:
|
|
var name_label := Label.new()
|
|
name_label.custom_minimum_size.x = 230.0
|
|
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
name_label.add_theme_font_size_override("font_size", 19)
|
|
name_label.add_theme_color_override("font_color", Color(0.18, 0.25, 0.31))
|
|
name_label.text = "%s:" % str(row.get("label", "Vital sign"))
|
|
vitals_rows.add_child(name_label)
|
|
var value_label := Label.new()
|
|
value_label.custom_minimum_size.x = 230.0
|
|
value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
value_label.add_theme_font_size_override("font_size", 19)
|
|
value_label.add_theme_color_override("font_color", Color(0.04, 0.09, 0.13))
|
|
value_label.text = str(row.get("value", ""))
|
|
value_label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
|
value_label.clip_text = true
|
|
vitals_rows.add_child(value_label)
|
|
vitals_rows.visible = not rows.is_empty()
|
|
vitals_empty.visible = rows.is_empty()
|
|
_resize_computer_windows()
|
|
|
|
|
|
func _build_vital_rows(vitals: Dictionary) -> Array[Dictionary]:
|
|
var rows: Array[Dictionary] = []
|
|
var displayed := {}
|
|
_append_vital(rows, displayed, vitals, "temperature", "Temperature")
|
|
_append_vital(rows, displayed, vitals, "pulse", "Pulse")
|
|
_append_vital(rows, displayed, vitals, "respirations", "Respiratory rate")
|
|
var blood_pressure := str(vitals.get("bloodPressure", "")).strip_edges()
|
|
if not blood_pressure.is_empty():
|
|
displayed["bloodPressure"] = true
|
|
var pattern := RegEx.create_from_string("^\\s*([^/]+)\\s*/\\s*([0-9.]+)(.*)$")
|
|
var matched := pattern.search(blood_pressure)
|
|
if matched:
|
|
var suffix := matched.get_string(3).strip_edges()
|
|
var unit_suffix := " %s" % suffix if not suffix.is_empty() else ""
|
|
rows.append({"label": "Blood pressure, systolic", "value": matched.get_string(1).strip_edges() + unit_suffix})
|
|
rows.append({"label": "Blood pressure, diastolic", "value": matched.get_string(2).strip_edges() + unit_suffix})
|
|
else:
|
|
rows.append({"label": "Blood pressure", "value": blood_pressure})
|
|
_append_vital(rows, displayed, vitals, "oxygenSaturation", "Oxygen saturation")
|
|
_append_vital(rows, displayed, vitals, "height", "Height")
|
|
_append_vital(rows, displayed, vitals, "weight", "Weight")
|
|
_append_vital(rows, displayed, vitals, "bmi", "Body Mass Index")
|
|
var remaining_keys: Array = vitals.keys()
|
|
remaining_keys.sort()
|
|
for raw_key in remaining_keys:
|
|
var key := str(raw_key)
|
|
if not displayed.has(key):
|
|
_append_vital(rows, displayed, vitals, key, _friendly_vital_name(key))
|
|
return rows
|
|
|
|
|
|
func _append_vital(
|
|
rows: Array[Dictionary], displayed: Dictionary, vitals: Dictionary,
|
|
key: String, label: String) -> void:
|
|
displayed[key] = true
|
|
var value := str(vitals.get(key, "")).strip_edges()
|
|
if not value.is_empty():
|
|
rows.append({"label": label, "value": value})
|
|
|
|
|
|
func _friendly_vital_name(key: String) -> String:
|
|
var result := ""
|
|
for index in key.length():
|
|
var character := key[index]
|
|
if index > 0 and character == character.to_upper() and character != character.to_lower():
|
|
result += " "
|
|
result += character
|
|
return result.capitalize()
|
|
|
|
|
|
func _style_vitals_window() -> void:
|
|
_style_computer_window(vitals_window)
|
|
|
|
|
|
func _style_computer_window(window: PanelContainer) -> void:
|
|
var panel_style := StyleBoxFlat.new()
|
|
panel_style.bg_color = Color(0.97, 0.98, 0.99, 0.98)
|
|
panel_style.border_color = Color(0.12, 0.24, 0.34, 0.9)
|
|
panel_style.set_border_width_all(3)
|
|
panel_style.set_corner_radius_all(14)
|
|
panel_style.shadow_color = Color(0.0, 0.0, 0.0, 0.35)
|
|
panel_style.shadow_size = 18
|
|
window.add_theme_stylebox_override("panel", panel_style)
|
|
|
|
|
|
func _style_desktop_shortcut(button: Button) -> void:
|
|
var normal := StyleBoxEmpty.new()
|
|
var hover := StyleBoxFlat.new()
|
|
hover.bg_color = Color(0.82, 0.91, 0.97, 0.18)
|
|
hover.border_color = Color(0.9, 0.96, 1.0, 0.5)
|
|
hover.set_border_width_all(1)
|
|
hover.set_corner_radius_all(8)
|
|
var pressed := hover.duplicate() as StyleBoxFlat
|
|
pressed.bg_color = Color(0.72, 0.86, 0.96, 0.28)
|
|
button.add_theme_stylebox_override("normal", normal)
|
|
button.add_theme_stylebox_override("hover", hover)
|
|
button.add_theme_stylebox_override("pressed", pressed)
|
|
button.add_theme_stylebox_override("focus", hover)
|
|
|
|
|
|
func _resize_computer_windows() -> void:
|
|
if vitals_window == null or medical_history_window == null:
|
|
return
|
|
var vital_row_count := vitals_rows.get_child_count() / 2
|
|
var vitals_content_height := clampf(180.0 + float(vital_row_count) * 36.0, 300.0, 540.0)
|
|
var vitals_size := Vector2(
|
|
minf(720.0, maxf(320.0, size.x - 48.0)),
|
|
minf(vitals_content_height, maxf(300.0, size.y - 48.0)))
|
|
_set_centered_window_rect(vitals_window, vitals_size, Vector2(-55.0, -25.0))
|
|
var exam_count := medical_history_exam_text.text.count("[b]") if medical_history_exam_text != null else 0
|
|
var showing_exams := medical_history_exam_scroll != null and medical_history_exam_scroll.visible
|
|
var history_height := 340.0 if not showing_exams else clampf(250.0 + exam_count * 105.0, 340.0, 650.0)
|
|
var history_size := Vector2(
|
|
minf(680.0, maxf(320.0, size.x - 48.0)),
|
|
minf(history_height, maxf(300.0, size.y - 48.0)))
|
|
_set_centered_window_rect(medical_history_window, history_size, Vector2(55.0, 25.0))
|
|
|
|
|
|
func _set_centered_window_rect(window: Control, window_size: Vector2, offset: Vector2) -> void:
|
|
window.offset_left = -window_size.x * 0.5 + offset.x
|
|
window.offset_top = -window_size.y * 0.5 + offset.y
|
|
window.offset_right = window_size.x * 0.5 + offset.x
|
|
window.offset_bottom = window_size.y * 0.5 + offset.y
|
|
_set_computer_window_position(window, window.position)
|
|
|
|
|
|
func _on_computer_window_header_input(
|
|
event: InputEvent, window: Control, close_button: Button) -> void:
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
if event.pressed:
|
|
if close_button.get_global_rect().has_point(event.global_position):
|
|
return
|
|
computer_drag_window = window
|
|
computer_drag_offset = event.global_position - window.global_position
|
|
_bring_computer_window_to_front(window)
|
|
else:
|
|
computer_drag_window = null
|
|
window.accept_event()
|
|
elif event is InputEventMouseMotion and computer_drag_window == window:
|
|
var target_global: Vector2 = event.global_position - computer_drag_offset
|
|
_set_computer_window_position(window, target_global - global_position)
|
|
window.accept_event()
|
|
|
|
|
|
func _on_computer_window_input(event: InputEvent, window: Control) -> void:
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
|
_bring_computer_window_to_front(window)
|
|
|
|
|
|
func _set_vitals_window_position(target: Vector2) -> void:
|
|
_set_computer_window_position(vitals_window, target)
|
|
|
|
|
|
func _set_medical_history_window_position(target: Vector2) -> void:
|
|
_set_computer_window_position(medical_history_window, target)
|
|
|
|
|
|
func _set_computer_window_position(window: Control, target: Vector2) -> void:
|
|
var maximum := Vector2(
|
|
maxf(0.0, size.x - window.size.x),
|
|
maxf(0.0, size.y - window.size.y))
|
|
window.position = Vector2(
|
|
clampf(target.x, 0.0, maximum.x),
|
|
clampf(target.y, 0.0, maximum.y))
|
|
|
|
|
|
func _bring_computer_window_to_front(window: Control) -> void:
|
|
computer_window_z += 1
|
|
window.z_index = computer_window_z
|
|
|
|
|
|
func _topmost_computer_window() -> Control:
|
|
var top_window: Control
|
|
for window in [vitals_window, medical_history_window, orders_window]:
|
|
if window.visible and (top_window == null or window.z_index > top_window.z_index):
|
|
top_window = window
|
|
return top_window
|
|
|
|
|
|
func _event_targets_computer_launcher(event: InputEvent) -> bool:
|
|
if not computer_launcher.visible:
|
|
return false
|
|
if event is InputEventMouseButton:
|
|
return event.button_index == MOUSE_BUTTON_LEFT \
|
|
and computer_launcher.get_global_rect().has_point(event.position)
|
|
if event is InputEventScreenTouch:
|
|
return computer_launcher.get_global_rect().has_point(event.position)
|
|
return false
|
|
|
|
|
|
func _split_dialogue_pages(text: String) -> Array[String]:
|
|
var normalized := text.strip_edges()
|
|
if normalized.is_empty():
|
|
return []
|
|
var pages: Array[String] = []
|
|
var current := ""
|
|
for word in normalized.split(" ", false):
|
|
var candidate := word if current.is_empty() else "%s %s" % [current, word]
|
|
var has_natural_break := current.length() >= DIALOGUE_PAGE_MIN_CHARS \
|
|
and _ends_dialogue_phrase(word)
|
|
if candidate.length() > DIALOGUE_PAGE_MAX_CHARS and not current.is_empty():
|
|
pages.append(current)
|
|
current = word
|
|
elif has_natural_break:
|
|
pages.append(candidate)
|
|
current = ""
|
|
else:
|
|
current = candidate
|
|
if not current.is_empty():
|
|
pages.append(current)
|
|
return pages
|
|
|
|
|
|
func _ends_dialogue_phrase(word: String) -> bool:
|
|
return word.ends_with(",") or word.ends_with(";") or word.ends_with(":") \
|
|
or word.ends_with(".") or word.ends_with("?") or word.ends_with("!")
|
|
|
|
|
|
func set_choices_pending(pending: bool) -> void:
|
|
for child in choice_container.get_children():
|
|
if child is BaseButton:
|
|
child.disabled = pending
|
|
previous_page_button.disabled = pending or choice_page <= 0
|
|
next_page_button.disabled = pending or choice_page >= _choice_page_count() - 1
|
|
|
|
|
|
func show_dialogue_choices(options: Array) -> void:
|
|
hide_dialogue_choices()
|
|
available_choices = options.duplicate(true)
|
|
choice_page = 0
|
|
_render_choice_page()
|
|
|
|
|
|
func _render_choice_page() -> void:
|
|
_clear_choice_buttons()
|
|
var page_count := _choice_page_count()
|
|
if page_count == 0:
|
|
choice_area.visible = false
|
|
return
|
|
choice_page = clampi(choice_page, 0, page_count - 1)
|
|
var start_index := choice_page * CHOICES_PER_PAGE
|
|
var end_index := mini(start_index + CHOICES_PER_PAGE, available_choices.size())
|
|
var visible_choice_count := end_index - start_index
|
|
for index in range(start_index, end_index):
|
|
var option: Dictionary = available_choices[index]
|
|
var choice_id := str(option.get("id", ""))
|
|
var option_text := str(option.get("text", ""))
|
|
var button := TextureButton.new()
|
|
button.set_meta("choice_id", choice_id)
|
|
button.custom_minimum_size = Vector2(
|
|
0.0 if visible_choice_count == CHOICES_PER_PAGE else 350.0, 136.0)
|
|
button.size_flags_horizontal = (
|
|
Control.SIZE_EXPAND_FILL if visible_choice_count == CHOICES_PER_PAGE
|
|
else Control.SIZE_SHRINK_CENTER)
|
|
button.ignore_texture_size = true
|
|
button.stretch_mode = TextureButton.STRETCH_SCALE
|
|
button.texture_normal = _crop_choice_texture(CHOICE_NORMAL)
|
|
button.texture_hover = _crop_choice_texture(CHOICE_HOVER)
|
|
button.texture_pressed = _crop_choice_texture(CHOICE_PRESSED)
|
|
button.self_modulate = Color(1.0, 1.0, 1.0, 0.86)
|
|
button.focus_mode = Control.FOCUS_ALL
|
|
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
|
button.mouse_entered.connect(_on_choice_hover.bind(button, true))
|
|
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(choice_id))
|
|
|
|
var label := Label.new()
|
|
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
label.offset_left = 28.0
|
|
label.offset_top = 12.0
|
|
label.offset_right = -28.0
|
|
label.offset_bottom = -12.0
|
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
label.add_theme_color_override("font_color", Color(0.055, 0.105, 0.145))
|
|
label.add_theme_font_size_override("font_size", _choice_font_size(option_text))
|
|
label.text = option_text
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
label.clip_text = true
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
button.add_child(label)
|
|
choice_container.add_child(button)
|
|
choice_pager.visible = page_count > 1
|
|
page_label.text = "Page %d of %d" % [choice_page + 1, page_count]
|
|
previous_page_button.disabled = choice_page <= 0
|
|
next_page_button.disabled = choice_page >= page_count - 1
|
|
choice_area.visible = true
|
|
choice_container.visible = true
|
|
_update_examination_launcher()
|
|
|
|
|
|
func hide_dialogue_choices() -> void:
|
|
_clear_choice_buttons()
|
|
available_choices = []
|
|
choice_page = 0
|
|
choice_container.visible = false
|
|
choice_pager.visible = false
|
|
choice_area.visible = false
|
|
_update_examination_launcher()
|
|
|
|
|
|
func _clear_choice_buttons() -> void:
|
|
for child in choice_container.get_children():
|
|
choice_container.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _choice_page_count() -> int:
|
|
return ceili(float(available_choices.size()) / float(CHOICES_PER_PAGE))
|
|
|
|
|
|
func _change_choice_page(direction: int) -> void:
|
|
choice_page = clampi(choice_page + direction, 0, maxi(_choice_page_count() - 1, 0))
|
|
_render_choice_page()
|
|
|
|
|
|
func _choice_font_size(text: String) -> int:
|
|
if text.length() > 105:
|
|
return 15
|
|
if text.length() > 75:
|
|
return 16
|
|
return 17
|
|
|
|
|
|
func _crop_choice_texture(source: Texture2D) -> AtlasTexture:
|
|
var cropped := AtlasTexture.new()
|
|
cropped.atlas = source
|
|
cropped.region = CHOICE_CROP
|
|
cropped.filter_clip = true
|
|
return cropped
|
|
|
|
|
|
func _on_choice_hover(button: TextureButton, hovered: bool) -> void:
|
|
button.self_modulate = (
|
|
Color(1.08, 1.05, 0.96, 0.94)
|
|
if hovered
|
|
else Color(1.0, 1.0, 1.0, 0.86)
|
|
)
|
|
|
|
|
|
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(choice_id: String) -> void:
|
|
if choice_id == "__continue":
|
|
show_exam_selector(true)
|
|
else:
|
|
choice_selected.emit(choice_id)
|
|
|
|
|
|
func show_physical_exam(exams: Array, chart: Array, pending_code: String = "") -> void:
|
|
available_exams = exams.duplicate(true)
|
|
attempt_chart = chart.duplicate(true)
|
|
_ensure_physical_exam_panel()
|
|
physical_exam_panel.visible = true
|
|
physical_exam_finding_title.visible = true
|
|
physical_exam_finding_text.visible = true
|
|
physical_exam_result_button.visible = false
|
|
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 configure_examinations(exams: Array, chart: Array) -> void:
|
|
available_exams = exams.duplicate(true)
|
|
attempt_chart = chart.duplicate(true)
|
|
_render_physical_exam_history()
|
|
_update_examination_launcher()
|
|
|
|
|
|
func show_exam_selector(terminal: bool = false) -> void:
|
|
if not choice_container.visible or (physical_exam_panel != null and physical_exam_panel.visible):
|
|
return
|
|
physical_exam_terminal = terminal
|
|
show_physical_exam(available_exams, attempt_chart)
|
|
physical_exam_finish_button.visible = terminal
|
|
physical_exam_close_button.visible = not terminal
|
|
|
|
|
|
func show_exam_result(chart: Array, exam_code: String) -> void:
|
|
attempt_chart = chart.duplicate(true)
|
|
_render_physical_exam_history()
|
|
_ensure_physical_exam_panel()
|
|
physical_exam_panel.visible = true
|
|
for child in physical_exam_grid.get_children():
|
|
child.visible = false
|
|
var finding := _chart_exam_entry(chart, exam_code)
|
|
physical_exam_finding_title.text = str(finding.get("title", "Examination Finding"))
|
|
physical_exam_finding_text.text = str(finding.get("text", "No finding was reported."))
|
|
physical_exam_result_button.visible = true
|
|
physical_exam_close_button.visible = false
|
|
physical_exam_finish_button.visible = false
|
|
|
|
|
|
func close_exam_overlay() -> void:
|
|
if physical_exam_panel != null:
|
|
physical_exam_panel.visible = false
|
|
_update_examination_launcher()
|
|
|
|
|
|
func _close_exam_result() -> void:
|
|
if physical_exam_terminal:
|
|
show_physical_exam(available_exams, attempt_chart)
|
|
physical_exam_finish_button.visible = true
|
|
physical_exam_close_button.visible = false
|
|
else:
|
|
close_exam_overlay()
|
|
|
|
|
|
func _chart_exam_entry(chart: Array, code: String) -> Dictionary:
|
|
for index in range(chart.size() - 1, -1, -1):
|
|
var entry: Dictionary = chart[index]
|
|
if str(entry.get("code", "")) == code:
|
|
return entry
|
|
return {}
|
|
|
|
|
|
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
|
|
physical_exam_panel.custom_minimum_size = Vector2(980.0, 650.0)
|
|
add_child(physical_exam_panel)
|
|
physical_exam_panel.set_anchors_preset(Control.PRESET_CENTER)
|
|
physical_exam_panel.offset_left = -490.0
|
|
physical_exam_panel.offset_top = -325.0
|
|
physical_exam_panel.offset_right = 490.0
|
|
physical_exam_panel.offset_bottom = 325.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 = "Review Final Orders"
|
|
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)
|
|
physical_exam_close_button = Button.new()
|
|
physical_exam_close_button.text = "Return to Interview"
|
|
physical_exam_close_button.custom_minimum_size = Vector2(190.0, 48.0)
|
|
physical_exam_close_button.pressed.connect(close_exam_overlay)
|
|
actions.add_child(physical_exam_close_button)
|
|
physical_exam_result_button = Button.new()
|
|
physical_exam_result_button.text = "Continue"
|
|
physical_exam_result_button.custom_minimum_size = Vector2(150.0, 48.0)
|
|
physical_exam_result_button.visible = false
|
|
physical_exam_result_button.pressed.connect(_close_exam_result)
|
|
actions.add_child(physical_exam_result_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
|
|
hide_patient_dialogue()
|
|
hide_dialogue_choices()
|
|
if debrief_panel != null and is_instance_valid(debrief_panel):
|
|
debrief_panel.queue_free()
|
|
debrief_panel = PanelContainer.new()
|
|
debrief_panel.name = "InterviewDebrief"
|
|
debrief_panel.z_index = 50
|
|
debrief_panel.size = Vector2(870.0, 600.0)
|
|
debrief_panel.position = (size - debrief_panel.size) * 0.5
|
|
var panel_style := StyleBoxFlat.new()
|
|
panel_style.bg_color = Color(0.96, 0.95, 0.91, 0.97)
|
|
panel_style.border_color = Color(0.12, 0.22, 0.28, 0.8)
|
|
panel_style.set_border_width_all(2)
|
|
panel_style.set_corner_radius_all(12)
|
|
panel_style.set_content_margin_all(28.0)
|
|
debrief_panel.add_theme_stylebox_override("panel", panel_style)
|
|
add_child(debrief_panel)
|
|
var content := VBoxContainer.new()
|
|
content.add_theme_constant_override("separation", 16)
|
|
debrief_panel.add_child(content)
|
|
var heading := Label.new()
|
|
heading.text = "Interview Debrief"
|
|
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 diagnosis := Label.new()
|
|
diagnosis.text = str(debrief.get("likelyDiagnosis", "Interview complete"))
|
|
diagnosis.add_theme_font_size_override("font_size", 23)
|
|
diagnosis.add_theme_color_override("font_color", Color(0.12, 0.36, 0.27))
|
|
diagnosis.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
content.add_child(diagnosis)
|
|
var summary := Label.new()
|
|
summary.text = str(debrief.get("summary", ""))
|
|
summary.add_theme_font_size_override("font_size", 18)
|
|
summary.add_theme_color_override("font_color", Color(0.08, 0.12, 0.15))
|
|
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
content.add_child(summary)
|
|
var score_label := Label.new()
|
|
score_label.text = "History coverage: %d%%" % int(round(float(score.get("percent", 0.0))))
|
|
score_label.add_theme_font_size_override("font_size", 20)
|
|
content.add_child(score_label)
|
|
var covered: Array[String] = []
|
|
var missed: Array[String] = []
|
|
for raw_criterion in score.get("criteria", []):
|
|
var criterion: Dictionary = raw_criterion
|
|
var title := str(criterion.get("title", "History topic"))
|
|
if str(criterion.get("status", "")) == "credited":
|
|
covered.append(title)
|
|
elif str(criterion.get("status", "")) == "missed":
|
|
missed.append(title)
|
|
var coverage := Label.new()
|
|
coverage.text = "Covered: %s\nMissed optional topics: %s" % [
|
|
", ".join(covered), "None" if missed.is_empty() else ", ".join(missed)]
|
|
coverage.add_theme_font_size_override("font_size", 16)
|
|
coverage.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
content.add_child(coverage)
|
|
var points := Label.new()
|
|
var teaching_points: Array = debrief.get("teachingPoints", [])
|
|
points.text = "Key points\n- " + "\n- ".join(teaching_points) if not teaching_points.is_empty() else ""
|
|
points.add_theme_font_size_override("font_size", 17)
|
|
points.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
points.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
content.add_child(points)
|
|
var actions := HBoxContainer.new()
|
|
actions.alignment = BoxContainer.ALIGNMENT_END
|
|
actions.add_theme_constant_override("separation", 12)
|
|
content.add_child(actions)
|
|
var return_button := Button.new()
|
|
return_button.text = "Return to Cases"
|
|
return_button.custom_minimum_size = Vector2(190.0, 48.0)
|
|
return_button.pressed.connect(func(): debrief_return_requested.emit())
|
|
actions.add_child(return_button)
|
|
var repeat_button := Button.new()
|
|
repeat_button.text = "Repeat Interview"
|
|
repeat_button.custom_minimum_size = Vector2(190.0, 48.0)
|
|
repeat_button.pressed.connect(func(): debrief_repeat_requested.emit())
|
|
actions.add_child(repeat_button)
|