Add clinical progression ranks and records

This commit is contained in:
hz
2026-08-01 15:17:42 -05:00
parent bc9f68aaf2
commit 8c5aa17d89
21 changed files with 631 additions and 10 deletions
@@ -553,6 +553,9 @@ func _apply_first_attempt_response(parsed: Dictionary) -> void:
_apply_rating_change(rating)
CreditsService.apply_award_response(parsed)
DailyMissionService.apply_response(parsed)
if typeof(parsed.get("progression")) == TYPE_DICTIONARY:
DashboardService.data["progression"] = parsed.get("progression")
DashboardService.dashboard_changed.emit()
# Let Tutor users inspect the final answer before the summary replaces the player.
StudySessionService.apply_response(parsed, true)
choice_percentages_updated.emit(question_id, percentages)
@@ -560,6 +563,9 @@ func _apply_first_attempt_response(parsed: Dictionary) -> void:
func _apply_skip_response(parsed: Dictionary) -> void:
if typeof(parsed.get("progression")) == TYPE_DICTIONARY:
DashboardService.data["progression"] = parsed.get("progression")
DashboardService.dashboard_changed.emit()
var question_id := str(parsed.get("questionExternalId", parsed.get("question_external_id", "")))
var correct_choice_id := str(parsed.get("correctChoiceId", parsed.get("correct_choice_id", "")))
var percentages: Dictionary = parsed.get("choiceSelectionPercentages", parsed.get("choice_selection_percentages", {}))
+3
View File
@@ -199,6 +199,9 @@ func _on_attempt_completed(result: int, response_code: int, _headers_value: Pack
if typeof(parsed) == TYPE_DICTIONARY:
CreditsService.apply_award_response(parsed)
DailyMissionService.apply_response(parsed)
if typeof(parsed.get("progression")) == TYPE_DICTIONARY:
DashboardService.data["progression"] = parsed.get("progression")
DashboardService.dashboard_changed.emit()
StudySessionService.apply_response(parsed, true)
pending_count = int(parsed.get("pendingCount", parsed.get("pending_count", pending_count)))
synced_question_id = str(parsed.get("questionExternalId", parsed.get("question_external_id", "")))
+39
View File
@@ -25,6 +25,7 @@ var recent_list: VBoxContainer
var weekly_summary: VBoxContainer
var mission_content: VBoxContainer
var weekly_goal_content: VBoxContainer
var rank_content: VBoxContainer
var metric_values: Dictionary = {}
var metric_details: Dictionary = {}
var metric_trends: Dictionary = {}
@@ -165,6 +166,9 @@ func _build() -> void:
right_column.add_theme_constant_override("separation", 12)
right_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
analytics_grid.add_child(right_column)
rank_content = VBoxContainer.new()
rank_content.add_theme_constant_override("separation", 9)
right_column.add_child(_card(rank_content, 16))
focus_content = VBoxContainer.new()
focus_content.add_theme_constant_override("separation", 10)
right_column.add_child(_card(focus_content, 16))
@@ -224,6 +228,7 @@ func _render() -> void:
_render_systems(data.get("systems", []))
_render_focus(data.get("systems", []))
_render_review(data)
_render_rank(data.get("progression", {}))
var weekly: Array = data.get("weeklyActivity", [])
activity_chart.set_days(weekly, UIThemeService.is_dark_mode())
_render_weekly(data, weekly)
@@ -232,6 +237,40 @@ func _render() -> void:
_render_weekly_goal()
func _render_rank(value: Variant) -> void:
if rank_content == null:
return
_clear(rank_content)
rank_content.add_child(_section_title("Clinical progression"))
var progression: Dictionary = value if typeof(value) == TYPE_DICTIONARY else {}
if progression.is_empty():
rank_content.add_child(_label("Loading rank progress...", 13, Brand.muted_color(UIThemeService.is_dark_mode())))
return
var title := _label(str(progression.get("displayName", "Case Scout III")), 23, Brand.PURPLE, true)
rank_content.add_child(title)
var next_name = progression.get("nextRankName")
if next_name == null:
rank_content.add_child(_label("Highest division earned", 12, Brand.SUCCESS, true))
else:
rank_content.add_child(_label("Next: %s %s" % [str(next_name), str(progression.get("nextDivision", ""))], 12, Brand.muted_color(UIThemeService.is_dark_mode())))
var requirements: Array = progression.get("nextRequirements", [])
for requirement_value in requirements.slice(0, mini(3, requirements.size())):
var requirement: Dictionary = requirement_value
var row := HBoxContainer.new()
var label := _label(str(requirement.get("label", "Progress")), 11, Brand.text_color(UIThemeService.is_dark_mode()))
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(label)
row.add_child(_label("%d / %d" % [int(requirement.get("current", 0)), int(requirement.get("required", 0))], 11,
Brand.SUCCESS if bool(requirement.get("complete", false)) else Brand.ROYAL_BLUE, true))
rank_content.add_child(row)
var view := Button.new()
view.text = "View ladder and records"
view.flat = true
view.alignment = HORIZONTAL_ALIGNMENT_LEFT
view.pressed.connect(func(): main.show_screen("profile"))
rank_content.add_child(view)
func _render_missions() -> void:
if mission_content == null:
return
+73
View File
@@ -21,6 +21,7 @@ var outcome_delete: Button
var connected_accounts_list: VBoxContainer
var return_screen := ""
var back_to_session_button: Button
var progression_content: VBoxContainer
func _ready() -> void:
@@ -95,6 +96,7 @@ func _build() -> void:
right.add_theme_constant_override("separation", 14)
columns.add_child(right)
right.add_child(_outcome_card())
right.add_child(_progression_card())
right.add_child(_connected_accounts_card())
right.add_child(_account_actions_card())
_refresh_back_to_session_visibility()
@@ -227,6 +229,13 @@ func _outcome_card() -> PanelContainer:
return _card(body)
func _progression_card() -> PanelContainer:
progression_content = VBoxContainer.new()
progression_content.add_theme_constant_override("separation", 10)
progression_content.add_child(_section_header("Clinical Progression", "Breadth, challenge, and retention"))
return _card(progression_content)
func _account_actions_card() -> PanelContainer:
var body := VBoxContainer.new()
body.add_theme_constant_override("separation", 9)
@@ -279,9 +288,73 @@ func _render() -> void:
_set_metric("cases", str(int(data.get("ccsCasesCompleted", 0))))
_set_metric("streak", "%d days" % int(data.get("currentStreak", 0)))
_render_systems(data.get("systems", []))
_render_progression(data.get("progression", {}))
_render_outcome()
func _render_progression(value: Variant) -> void:
if progression_content == null:
return
_clear(progression_content)
progression_content.add_child(_section_header("Clinical Progression", "Breadth, challenge, and retention"))
var progression: Dictionary = value if typeof(value) == TYPE_DICTIONARY else {}
if progression.is_empty():
progression_content.add_child(_label("Loading progression...", 13, Brand.muted_color(_dark())))
return
progression_content.add_child(_label(str(progression.get("displayName", "Case Scout III")), 24, Brand.PURPLE, true))
var badges: Array = progression.get("earnedBadges", [])
progression_content.add_child(_label("Earned: %s" % ", ".join(PackedStringArray(badges)), 12, Brand.muted_color(_dark())))
var ladder := GridContainer.new()
ladder.columns = 3
ladder.add_theme_constant_override("h_separation", 7)
ladder.add_theme_constant_override("v_separation", 7)
var rank_names := ["Case Scout", "Pattern Hunter", "Diagnostic Tactician", "Management Strategist", "Clinical Vanguard"]
var divisions := ["III", "II", "I"]
var earned_ordinal := int(progression.get("rankOrdinal", 0))
var earned_dates: Dictionary = progression.get("earnedRanksUtc", {})
var ladder_data: Array = progression.get("ladder", [])
for ordinal in 15:
var earned := ordinal <= earned_ordinal
var caption := "%s %s" % [rank_names[floori(float(ordinal) / 3.0)], divisions[ordinal % 3]]
var date := str(earned_dates.get(str(ordinal), "")).left(10)
if earned and not date.is_empty():
caption += "\n%s" % date
var cell := _label(caption, 10, Brand.SUCCESS if earned else Brand.muted_color(_dark()), earned)
cell.custom_minimum_size = Vector2(105, 38)
cell.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
cell.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
if ordinal < ladder_data.size() and typeof(ladder_data[ordinal]) == TYPE_DICTIONARY:
cell.tooltip_text = str((ladder_data[ordinal] as Dictionary).get("requirementSummary", ""))
ladder.add_child(cell)
progression_content.add_child(ladder)
var next_name = progression.get("nextRankName")
if next_name != null:
progression_content.add_child(_label("Next: %s %s" % [str(next_name), str(progression.get("nextDivision", ""))], 14, Brand.text_color(_dark()), true))
for requirement_value in progression.get("nextRequirements", []):
var requirement: Dictionary = requirement_value
var row := HBoxContainer.new()
var text := _label(str(requirement.get("label", "Requirement")), 11, Brand.text_color(_dark()))
text.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(text)
row.add_child(_label("%d / %d" % [int(requirement.get("current", 0)), int(requirement.get("required", 0))], 11,
Brand.SUCCESS if bool(requirement.get("complete", false)) else Brand.ROYAL_BLUE, true))
progression_content.add_child(row)
progression_content.add_child(HSeparator.new())
progression_content.add_child(_label("Personal records", 15, Brand.text_color(_dark()), true))
for record_value in progression.get("records", []):
var record: Dictionary = record_value
var suffix := "%d%%" % int(record.get("value", 0)) if str(record.get("key", "")) == "timed_block" else str(int(record.get("value", 0)))
var title := _label("%s %s" % [str(record.get("label", "Record")), suffix], 12, Brand.text_color(_dark()), true)
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
progression_content.add_child(title)
var definition := str(record.get("definition", ""))
if bool(record.get("trackingFromLaunch", false)):
definition += " Tracking since %s." % str(progression.get("trackingStartedUtc", "")).left(10)
var detail := _label(definition, 10, Brand.muted_color(_dark()))
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
progression_content.add_child(detail)
func _render_connected_accounts(provider_values: Variant, available_values: Variant) -> void:
if connected_accounts_list == null:
return