Make graphical CCS dialogue choice first
This commit is contained in:
@@ -229,11 +229,15 @@ public sealed class CcsCaseTests
|
|||||||
definition.InitialInterview = TopicInterview();
|
definition.InitialInterview = TopicInterview();
|
||||||
var attempt = new CcsAttempt { InterviewNodeId = "opening", OpeningCompleted = false };
|
var attempt = new CcsAttempt { InterviewNodeId = "opening", OpeningCompleted = false };
|
||||||
|
|
||||||
Assert.Equal(["chief_concern"], CcsStore.CurrentInterviewNode(attempt, definition)!.Choices.Select(value => value.Id));
|
var opening = CcsStore.CurrentInterviewNode(attempt, definition)!;
|
||||||
|
Assert.True(opening.SuppressPatientText);
|
||||||
|
Assert.Equal(["chief_concern"], opening.Choices.Select(value => value.Id));
|
||||||
Assert.Throws<ArgumentException>(() => CcsStore.ApplyDialogueChoice(attempt, definition, "required_history", DateTime.UtcNow));
|
Assert.Throws<ArgumentException>(() => CcsStore.ApplyDialogueChoice(attempt, definition, "required_history", DateTime.UtcNow));
|
||||||
CcsStore.ApplyDialogueChoice(attempt, definition, "chief_concern", DateTime.UtcNow);
|
CcsStore.ApplyDialogueChoice(attempt, definition, "chief_concern", DateTime.UtcNow);
|
||||||
Assert.Equal(["required_history", "optional_history"],
|
var chiefConcern = CcsStore.CurrentInterviewNode(attempt, definition)!;
|
||||||
CcsStore.CurrentInterviewNode(attempt, definition)!.Choices.Select(value => value.Id));
|
Assert.False(chiefConcern.SuppressPatientText);
|
||||||
|
Assert.Equal("I feel tired.", chiefConcern.PatientText);
|
||||||
|
Assert.Equal(["required_history", "optional_history"], chiefConcern.Choices.Select(value => value.Id));
|
||||||
Assert.Throws<ArgumentException>(() => CcsStore.ApplyDialogueChoice(attempt, definition, "finish_interview", DateTime.UtcNow));
|
Assert.Throws<ArgumentException>(() => CcsStore.ApplyDialogueChoice(attempt, definition, "finish_interview", DateTime.UtcNow));
|
||||||
|
|
||||||
CcsStore.ApplyDialogueChoice(attempt, definition, "required_history", DateTime.UtcNow);
|
CcsStore.ApplyDialogueChoice(attempt, definition, "required_history", DateTime.UtcNow);
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ public class CcsDialogueNode
|
|||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
public string PatientText { get; set; } = "";
|
public string PatientText { get; set; } = "";
|
||||||
|
public bool SuppressPatientText { get; set; }
|
||||||
public bool IsTerminal { get; set; }
|
public bool IsTerminal { get; set; }
|
||||||
public List<CcsDialogueChoice> Choices { get; set; } = [];
|
public List<CcsDialogueChoice> Choices { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -793,7 +793,9 @@ public sealed class CcsStore
|
|||||||
});
|
});
|
||||||
return new CcsDialogueNode
|
return new CcsDialogueNode
|
||||||
{
|
{
|
||||||
Id = attempt.InterviewNodeId, PatientText = patientText, Choices = choices
|
Id = attempt.InterviewNodeId, PatientText = patientText,
|
||||||
|
SuppressPatientText = attempt.InterviewNodeId.Equals("opening", StringComparison.OrdinalIgnoreCase),
|
||||||
|
Choices = choices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var interview = EffectiveInterview(definition);
|
var interview = EffectiveInterview(definition);
|
||||||
@@ -848,7 +850,7 @@ public sealed class CcsStore
|
|||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = "opening", PatientText = "Hello, doctor.",
|
Id = "opening", PatientText = "Hello, doctor.", SuppressPatientText = true,
|
||||||
Choices = [new() { Id = "ask_reason", Text = "What brings you in today?", NextNodeId = "reason" }]
|
Choices = [new() { Id = "ask_reason", Text = "What brings you in today?", NextNodeId = "reason" }]
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ var dialogue_tween: Tween
|
|||||||
var clock_seconds := 9.0 * 60.0 * 60.0
|
var clock_seconds := 9.0 * 60.0 * 60.0
|
||||||
var debrief_panel: PanelContainer
|
var debrief_panel: PanelContainer
|
||||||
var experience_mode := "management"
|
var experience_mode := "management"
|
||||||
|
var pending_choices: Array = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_update_clock()
|
_update_clock()
|
||||||
_schedule_patient_blink()
|
_schedule_patient_blink()
|
||||||
show_patient_dialogue("Loading patient interview...")
|
hide_patient_dialogue()
|
||||||
hide_dialogue_choices()
|
hide_dialogue_choices()
|
||||||
|
|
||||||
|
|
||||||
@@ -89,12 +90,14 @@ func show_patient_dialogue(text: String, characters_per_second: float = 35.0) ->
|
|||||||
var reveal_duration := float(text.length()) / maxf(characters_per_second, 1.0)
|
var reveal_duration := float(text.length()) / maxf(characters_per_second, 1.0)
|
||||||
dialogue_tween = create_tween()
|
dialogue_tween = create_tween()
|
||||||
dialogue_tween.tween_property(dialogue_text, "visible_ratio", 1.0, reveal_duration)
|
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:
|
func hide_patient_dialogue() -> void:
|
||||||
if dialogue_tween and dialogue_tween.is_valid():
|
if dialogue_tween and dialogue_tween.is_valid():
|
||||||
dialogue_tween.kill()
|
dialogue_tween.kill()
|
||||||
dialogue_bubble.visible = false
|
dialogue_bubble.visible = false
|
||||||
|
pending_choices = []
|
||||||
|
|
||||||
|
|
||||||
func is_patient_dialogue_visible() -> bool:
|
func is_patient_dialogue_visible() -> bool:
|
||||||
@@ -113,14 +116,35 @@ func configure_interview(node: Dictionary) -> void:
|
|||||||
if debrief_panel != null and is_instance_valid(debrief_panel):
|
if debrief_panel != null and is_instance_valid(debrief_panel):
|
||||||
debrief_panel.queue_free()
|
debrief_panel.queue_free()
|
||||||
debrief_panel = null
|
debrief_panel = null
|
||||||
show_patient_dialogue(str(node.get("patientText", "")))
|
var next_choices: Array
|
||||||
if bool(node.get("isTerminal", false)):
|
if bool(node.get("isTerminal", false)):
|
||||||
show_dialogue_choices([{
|
next_choices = [{
|
||||||
"id": "__continue",
|
"id": "__continue",
|
||||||
"text": "Finish Interview" if experience_mode == "graphical_interview" else "Continue to Patient Management"
|
"text": "Finish Interview" if experience_mode == "graphical_interview" else "Continue to Patient Management"
|
||||||
}])
|
}]
|
||||||
else:
|
else:
|
||||||
show_dialogue_choices(node.get("choices", []))
|
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_tween and dialogue_tween.is_valid():
|
||||||
|
dialogue_tween.kill()
|
||||||
|
dialogue_text.visible_ratio = 1.0
|
||||||
|
_on_dialogue_reveal_finished()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_dialogue_reveal_finished() -> void:
|
||||||
|
dialogue_tween = null
|
||||||
|
var choices := pending_choices
|
||||||
|
pending_choices = []
|
||||||
|
show_dialogue_choices(choices)
|
||||||
|
|
||||||
|
|
||||||
func set_choices_pending(pending: bool) -> void:
|
func set_choices_pending(pending: bool) -> void:
|
||||||
|
|||||||
@@ -92,18 +92,18 @@ func _render_state() -> void:
|
|||||||
|
|
||||||
func _render_attempt(attempt: Dictionary) -> void:
|
func _render_attempt(attempt: Dictionary) -> void:
|
||||||
if str(attempt.get("state", "")) != "active":
|
if str(attempt.get("state", "")) != "active":
|
||||||
_handoff_to_classic_player()
|
_show_unsupported_attempt("This active case is not available in the graphical interview yet.")
|
||||||
return
|
return
|
||||||
var interview: Dictionary = attempt.get("interview", {})
|
var interview: Dictionary = attempt.get("interview", {})
|
||||||
if interview.is_empty():
|
if interview.is_empty():
|
||||||
_handoff_to_classic_player()
|
_show_unsupported_attempt("This active case does not include a graphical dialogue interview.")
|
||||||
return
|
return
|
||||||
var node: Dictionary = interview.get("currentNode", {})
|
var node: Dictionary = interview.get("currentNode", {})
|
||||||
if node.is_empty():
|
if node.is_empty():
|
||||||
_handoff_to_classic_player()
|
_show_unsupported_attempt("The graphical interview could not load its current dialogue.")
|
||||||
return
|
return
|
||||||
if bool(attempt.get("openingCompleted", false)) and not bool(node.get("isTerminal", false)):
|
if bool(attempt.get("openingCompleted", false)) and not bool(node.get("isTerminal", false)):
|
||||||
_handoff_to_classic_player()
|
_show_unsupported_attempt("This case has progressed beyond the graphical interview.")
|
||||||
return
|
return
|
||||||
_show_doctor_office(node, attempt)
|
_show_doctor_office(node, attempt)
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ func _build_selection_view() -> void:
|
|||||||
heading.add_theme_font_size_override("font_size", 28)
|
heading.add_theme_font_size_override("font_size", 28)
|
||||||
selection_view.add_child(heading)
|
selection_view.add_child(heading)
|
||||||
var description := Label.new()
|
var description := Label.new()
|
||||||
description.text = "Begin a timed patient interview, then continue into clinical management."
|
description.text = "Interview the patient by selecting the questions you want to ask."
|
||||||
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
Brand.apply_font(description)
|
Brand.apply_font(description)
|
||||||
description.add_theme_font_size_override("font_size", 16)
|
description.add_theme_font_size_override("font_size", 16)
|
||||||
@@ -158,6 +158,12 @@ func _show_selection(message: String = "") -> void:
|
|||||||
retry_button.visible = false
|
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:
|
func _render_case_selection() -> void:
|
||||||
_show_selection()
|
_show_selection()
|
||||||
_clear_children(case_list)
|
_clear_children(case_list)
|
||||||
|
|||||||
@@ -59,12 +59,15 @@ func _run() -> void:
|
|||||||
screen._render_state()
|
screen._render_state()
|
||||||
await process_frame
|
await process_frame
|
||||||
_assert(screen.current_view == "doctor_office", "an active interactive attempt should open the doctor office")
|
_assert(screen.current_view == "doctor_office", "an active interactive attempt should open the doctor office")
|
||||||
_assert(screen.active_scene.dialogue_text.text == "Hello, doctor.", "the patient bubble should use the server-provided current node")
|
_assert(not screen.active_scene.is_patient_dialogue_visible(), "the opening should wait for a clinician dialogue selection")
|
||||||
_assert(screen.active_scene.choice_container.get_child_count() == 1, "the scene should render server-provided dialogue choices")
|
_assert(screen.active_scene.choice_container.get_child_count() == 1, "the scene should render server-provided dialogue choices")
|
||||||
_assert(is_equal_approx(screen.stage_root.scale.x, screen.stage_root.scale.y), "the graphical stage should preserve its aspect ratio")
|
_assert(is_equal_approx(screen.stage_root.scale.x, screen.stage_root.scale.y), "the graphical stage should preserve its aspect ratio")
|
||||||
service.attempt = _sample_branching_attempt()
|
service.attempt = _sample_branching_attempt()
|
||||||
screen._render_state()
|
screen._render_state()
|
||||||
await process_frame
|
await process_frame
|
||||||
|
_assert(screen.active_scene.is_patient_dialogue_visible(), "a selected topic should display the patient's response")
|
||||||
|
_assert(not screen.active_scene.choice_container.visible, "the next choices should wait until the patient finishes speaking")
|
||||||
|
screen.active_scene.complete_dialogue_reveal()
|
||||||
_assert(screen.active_scene.choice_container.columns == 2, "branching interview choices should use two columns")
|
_assert(screen.active_scene.choice_container.columns == 2, "branching interview choices should use two columns")
|
||||||
_assert(screen.active_scene.choice_container.get_child_count() == 8, "all available branching topics should render")
|
_assert(screen.active_scene.choice_container.get_child_count() == 8, "all available branching topics should render")
|
||||||
|
|
||||||
@@ -72,10 +75,19 @@ func _run() -> void:
|
|||||||
screen._on_attempt_changed()
|
screen._on_attempt_changed()
|
||||||
await process_frame
|
await process_frame
|
||||||
_assert(screen.active_scene.dialogue_text.text == "Okay, doctor.", "attempt updates should advance the rendered patient response")
|
_assert(screen.active_scene.dialogue_text.text == "Okay, doctor.", "attempt updates should advance the rendered patient response")
|
||||||
|
_assert(not screen.active_scene.choice_container.visible, "the terminal action should wait for the closing response")
|
||||||
|
screen.active_scene.complete_dialogue_reveal()
|
||||||
_assert(screen.active_scene.choice_container.get_child_count() == 1, "terminal dialogue should expose the management handoff")
|
_assert(screen.active_scene.choice_container.get_child_count() == 1, "terminal dialogue should expose the management handoff")
|
||||||
screen.active_scene.interview_continue_requested.emit()
|
screen.active_scene.interview_continue_requested.emit()
|
||||||
_assert(host.shown_screen == "ccs_player", "terminal dialogue should hand off the same attempt to the CCS player")
|
_assert(host.shown_screen == "ccs_player", "terminal dialogue should hand off the same attempt to the CCS player")
|
||||||
|
|
||||||
|
service.attempt = {"id": "classic-attempt", "state": "active", "openingCompleted": false, "interview": {}}
|
||||||
|
host.shown_screen = ""
|
||||||
|
screen._on_attempt_changed()
|
||||||
|
await process_frame
|
||||||
|
_assert(screen.current_view == "case_selection", "unsupported active attempts should remain outside the classic player")
|
||||||
|
_assert(host.shown_screen.is_empty(), "CCS Cases 2 should not trigger classic opening overlays")
|
||||||
|
|
||||||
service.attempt = _sample_graphical_debrief()
|
service.attempt = _sample_graphical_debrief()
|
||||||
screen._on_attempt_changed()
|
screen._on_attempt_changed()
|
||||||
await process_frame
|
await process_frame
|
||||||
@@ -98,6 +110,7 @@ func _sample_attempt(completed: bool) -> Dictionary:
|
|||||||
"id": "done" if completed else "greeting",
|
"id": "done" if completed else "greeting",
|
||||||
"patientText": "Okay, doctor." if completed else "Hello, doctor.",
|
"patientText": "Okay, doctor." if completed else "Hello, doctor.",
|
||||||
"isTerminal": completed,
|
"isTerminal": completed,
|
||||||
|
"suppressPatientText": not completed,
|
||||||
"choices": [] if completed else [{"id": "ask_reason", "text": "What brings you in?", "nextNodeId": "done"}]
|
"choices": [] if completed else [{"id": "ask_reason", "text": "What brings you in?", "nextNodeId": "done"}]
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user