Show rank tier and ELO alongside RZ points in the coins badge
Adds a server-computed tier/division (Bronze..Diamond, III-I) to the ratings profile endpoint and surfaces it next to the numeric ELO in the shared header badge, matching the coin balance display already there.
This commit is contained in:
@@ -47,4 +47,6 @@ public class RatingProfileResponse
|
|||||||
public int OverallElo { get; set; }
|
public int OverallElo { get; set; }
|
||||||
public int RatedQuestionCount { get; set; }
|
public int RatedQuestionCount { get; set; }
|
||||||
public Dictionary<string, int> CategoryElos { get; set; } = [];
|
public Dictionary<string, int> CategoryElos { get; set; } = [];
|
||||||
|
public string Tier { get; set; } = "";
|
||||||
|
public int TierDivision { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,43 @@ public class RatingStore
|
|||||||
public async Task<RatingProfileResponse> GetProfileAsync(string userId)
|
public async Task<RatingProfileResponse> GetProfileAsync(string userId)
|
||||||
{
|
{
|
||||||
var rating = await GetOrCreateUserRatingAsync(userId);
|
var rating = await GetOrCreateUserRatingAsync(userId);
|
||||||
|
var (tierName, tierDivision) = GetTier(rating.OverallElo);
|
||||||
return new RatingProfileResponse
|
return new RatingProfileResponse
|
||||||
{
|
{
|
||||||
OverallElo = rating.OverallElo,
|
OverallElo = rating.OverallElo,
|
||||||
RatedQuestionCount = rating.RatedQuestionCount,
|
RatedQuestionCount = rating.RatedQuestionCount,
|
||||||
CategoryElos = rating.CategoryElos
|
CategoryElos = rating.CategoryElos,
|
||||||
|
Tier = tierName,
|
||||||
|
TierDivision = tierDivision
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly (string Name, int FloorElo)[] TierBands =
|
||||||
|
[
|
||||||
|
("Bronze", MinElo),
|
||||||
|
("Silver", 1000),
|
||||||
|
("Gold", 1300),
|
||||||
|
("Platinum", 1900),
|
||||||
|
("Diamond", 2200)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static (string Name, int Division) GetTier(int elo)
|
||||||
|
{
|
||||||
|
var bandIndex = 0;
|
||||||
|
for (var i = 0; i < TierBands.Length; i++)
|
||||||
|
{
|
||||||
|
if (elo >= TierBands[i].FloorElo)
|
||||||
|
bandIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
var band = TierBands[bandIndex];
|
||||||
|
var nextFloor = bandIndex + 1 < TierBands.Length ? TierBands[bandIndex + 1].FloorElo : band.FloorElo + 300;
|
||||||
|
var bandWidth = nextFloor - band.FloorElo;
|
||||||
|
var progress = bandWidth > 0 ? (elo - band.FloorElo) / (double)bandWidth : 0;
|
||||||
|
var division = 3 - Math.Clamp((int)(progress * 3), 0, 2);
|
||||||
|
return (band.Name, division);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<UserRating> GetOrInitializeHistoryAsync(string userId, DateTime nowUtc)
|
public async Task<UserRating> GetOrInitializeHistoryAsync(string userId, DateTime nowUtc)
|
||||||
{
|
{
|
||||||
var rating = await GetOrCreateUserRatingAsync(userId);
|
var rating = await GetOrCreateUserRatingAsync(userId);
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.79 17 5 19 5a1 1 0 0 1 1 1z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 381 B |
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b6wxo6lr3s8ll"
|
||||||
|
path="res://.godot/imported/shield.svg-fbfc812301b797aa0266f7b4e83e6ef3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/icons/lucide/shield.svg"
|
||||||
|
dest_files=["res://.godot/imported/shield.svg-fbfc812301b797aa0266f7b4e83e6ef3.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
@@ -38,6 +38,8 @@ var load_state := "idle"
|
|||||||
var retry_attempt := 0
|
var retry_attempt := 0
|
||||||
var next_retry_seconds := 0
|
var next_retry_seconds := 0
|
||||||
var overall_elo := 1000
|
var overall_elo := 1000
|
||||||
|
var rank_tier := "Bronze"
|
||||||
|
var rank_division := 3
|
||||||
var rated_question_count := 0
|
var rated_question_count := 0
|
||||||
var category_elos := {}
|
var category_elos := {}
|
||||||
var last_rating_change := {}
|
var last_rating_change := {}
|
||||||
@@ -246,6 +248,8 @@ func clear_questions() -> void:
|
|||||||
retry_attempt = 0
|
retry_attempt = 0
|
||||||
next_retry_seconds = 0
|
next_retry_seconds = 0
|
||||||
overall_elo = 1000
|
overall_elo = 1000
|
||||||
|
rank_tier = "Bronze"
|
||||||
|
rank_division = 3
|
||||||
rated_question_count = 0
|
rated_question_count = 0
|
||||||
category_elos = {}
|
category_elos = {}
|
||||||
last_rating_change = {}
|
last_rating_change = {}
|
||||||
@@ -617,6 +621,8 @@ func _on_ratings_loaded(result: int, response_code: int, _headers: PackedStringA
|
|||||||
return
|
return
|
||||||
|
|
||||||
overall_elo = int(parsed.get("overallElo", parsed.get("overall_elo", 1000)))
|
overall_elo = int(parsed.get("overallElo", parsed.get("overall_elo", 1000)))
|
||||||
|
rank_tier = str(parsed.get("tier", rank_tier))
|
||||||
|
rank_division = int(parsed.get("tierDivision", parsed.get("tier_division", rank_division)))
|
||||||
rated_question_count = int(parsed.get("ratedQuestionCount", parsed.get("rated_question_count", 0)))
|
rated_question_count = int(parsed.get("ratedQuestionCount", parsed.get("rated_question_count", 0)))
|
||||||
category_elos = parsed.get("categoryElos", parsed.get("category_elos", {}))
|
category_elos = parsed.get("categoryElos", parsed.get("category_elos", {}))
|
||||||
ratings_changed.emit()
|
ratings_changed.emit()
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
extends PanelContainer
|
extends PanelContainer
|
||||||
|
|
||||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||||
|
const SHIELD_ICON := preload("res://assets/icons/lucide/shield.svg")
|
||||||
const STAR_ICON := preload("res://assets/icons/lucide/star.svg")
|
const STAR_ICON := preload("res://assets/icons/lucide/star.svg")
|
||||||
const CHEVRON_ICON := preload("res://assets/icons/lucide/chevron-down.svg")
|
const CHEVRON_ICON := preload("res://assets/icons/lucide/chevron-down.svg")
|
||||||
|
|
||||||
|
var rank_icon_panel: PanelContainer
|
||||||
|
var rank_icon_texture: TextureRect
|
||||||
|
var rank_tier_label: Label
|
||||||
|
var rank_elo_label: Label
|
||||||
|
var divider: ColorRect
|
||||||
|
|
||||||
var icon_panel: PanelContainer
|
var icon_panel: PanelContainer
|
||||||
var icon_texture: TextureRect
|
var icon_texture: TextureRect
|
||||||
var value_label: Label
|
var value_label: Label
|
||||||
@@ -13,12 +20,14 @@ var _initialized := false
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
custom_minimum_size = Vector2(150, 44)
|
custom_minimum_size = Vector2(230, 44)
|
||||||
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
_build_ui()
|
_build_ui()
|
||||||
refresh_theme_controls()
|
refresh_theme_controls()
|
||||||
_on_credits_changed()
|
_on_credits_changed()
|
||||||
|
_on_ratings_changed()
|
||||||
CreditsService.credits_changed.connect(_on_credits_changed)
|
CreditsService.credits_changed.connect(_on_credits_changed)
|
||||||
|
QuestionBankService.ratings_changed.connect(_on_ratings_changed)
|
||||||
resized.connect(func(): pivot_offset = size * 0.5)
|
resized.connect(func(): pivot_offset = size * 0.5)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +43,35 @@ func _build_ui() -> void:
|
|||||||
row.add_theme_constant_override("separation", 8)
|
row.add_theme_constant_override("separation", 8)
|
||||||
margin.add_child(row)
|
margin.add_child(row)
|
||||||
|
|
||||||
|
rank_icon_panel = PanelContainer.new()
|
||||||
|
rank_icon_panel.custom_minimum_size = Vector2(28, 28)
|
||||||
|
rank_icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
rank_icon_texture = TextureRect.new()
|
||||||
|
rank_icon_texture.texture = SHIELD_ICON
|
||||||
|
rank_icon_texture.custom_minimum_size = Vector2(16, 16)
|
||||||
|
rank_icon_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
rank_icon_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
rank_icon_panel.add_child(rank_icon_texture)
|
||||||
|
row.add_child(rank_icon_panel)
|
||||||
|
|
||||||
|
var rank_copy := VBoxContainer.new()
|
||||||
|
rank_copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
rank_copy.add_theme_constant_override("separation", 0)
|
||||||
|
rank_tier_label = Label.new()
|
||||||
|
rank_tier_label.add_theme_font_override("font", Brand.font)
|
||||||
|
rank_tier_label.add_theme_font_size_override("font_size", 15)
|
||||||
|
rank_copy.add_child(rank_tier_label)
|
||||||
|
rank_elo_label = Label.new()
|
||||||
|
rank_elo_label.add_theme_font_override("font", Brand.font)
|
||||||
|
rank_elo_label.add_theme_font_size_override("font_size", 10)
|
||||||
|
rank_copy.add_child(rank_elo_label)
|
||||||
|
row.add_child(rank_copy)
|
||||||
|
|
||||||
|
divider = ColorRect.new()
|
||||||
|
divider.custom_minimum_size = Vector2(1, 26)
|
||||||
|
divider.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
row.add_child(divider)
|
||||||
|
|
||||||
icon_panel = PanelContainer.new()
|
icon_panel = PanelContainer.new()
|
||||||
icon_panel.custom_minimum_size = Vector2(28, 28)
|
icon_panel.custom_minimum_size = Vector2(28, 28)
|
||||||
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
@@ -53,7 +91,7 @@ func _build_ui() -> void:
|
|||||||
value_label.add_theme_font_size_override("font_size", 15)
|
value_label.add_theme_font_size_override("font_size", 15)
|
||||||
copy.add_child(value_label)
|
copy.add_child(value_label)
|
||||||
caption_label = Label.new()
|
caption_label = Label.new()
|
||||||
caption_label.text = "Rz Coins"
|
caption_label.text = "RZ Points"
|
||||||
caption_label.add_theme_font_override("font", Brand.font)
|
caption_label.add_theme_font_override("font", Brand.font)
|
||||||
caption_label.add_theme_font_size_override("font_size", 10)
|
caption_label.add_theme_font_size_override("font_size", 10)
|
||||||
copy.add_child(caption_label)
|
copy.add_child(caption_label)
|
||||||
@@ -71,7 +109,12 @@ func _build_ui() -> void:
|
|||||||
|
|
||||||
func refresh_theme_controls() -> void:
|
func refresh_theme_controls() -> void:
|
||||||
var dark := UIThemeService.is_dark_mode()
|
var dark := UIThemeService.is_dark_mode()
|
||||||
add_theme_stylebox_override("panel", Brand.content_card_box(dark, false, Brand.AMBER))
|
add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE))
|
||||||
|
rank_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 14))
|
||||||
|
rank_icon_texture.modulate = Brand.ROYAL_BLUE
|
||||||
|
rank_tier_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||||
|
rank_elo_label.add_theme_color_override("font_color", Brand.ROYAL_BLUE)
|
||||||
|
divider.color = Color("2a365a") if dark else Brand.LIGHT_BORDER
|
||||||
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.AMBER, 14))
|
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.AMBER, 14))
|
||||||
icon_texture.modulate = Brand.AMBER
|
icon_texture.modulate = Brand.AMBER
|
||||||
value_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
value_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||||
@@ -84,6 +127,13 @@ func bump() -> void:
|
|||||||
tween.tween_property(self, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
tween.tween_property(self, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ratings_changed() -> void:
|
||||||
|
var roman := {1: "I", 2: "II", 3: "III"}
|
||||||
|
var division_label: String = roman.get(QuestionBankService.rank_division, "")
|
||||||
|
rank_tier_label.text = "%s %s" % [QuestionBankService.rank_tier, division_label]
|
||||||
|
rank_elo_label.text = "%d ELO" % QuestionBankService.overall_elo
|
||||||
|
|
||||||
|
|
||||||
func _on_credits_changed() -> void:
|
func _on_credits_changed() -> void:
|
||||||
if not CreditsService.loaded:
|
if not CreditsService.loaded:
|
||||||
_initialized = false
|
_initialized = false
|
||||||
|
|||||||
Reference in New Issue
Block a user