Gate Rz Coins on correct-first-try-within-time-limit; match coin-flight animation to it
Credits were previously awarded on any first attempt regardless of correctness or timing. The award now requires the first-ever attempt to be correct (record.Attempt.IsCorrect) and submitted before the adaptive per-question timer expired (new MetTimeLimit request field, computed client-side from the same elapsed/target comparison that already drives the visible countdown bar). The coin-flight animation in question_player.gd now only plays when both conditions are met (BlockSessionService.submit_core_attempt's existing "mastered" flag plus met_time_limit), so the effect never promises a coin that isn't actually earned. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -175,7 +175,7 @@ public class QuestionsController : ControllerBase
|
||||
_metrics.AttemptDuplicate();
|
||||
}
|
||||
await _reviewQueue.AddFromFirstAttemptAsync(record.Attempt);
|
||||
if (record.Inserted && !User.IsInRole("SUPER"))
|
||||
if (record.Inserted && record.Attempt.IsCorrect && req.MetTimeLimit && !User.IsInRole("SUPER"))
|
||||
await _credits.AwardForFirstAnswerAsync(userId, record.Attempt.Id ?? "");
|
||||
|
||||
var ratingOwner = Guid.NewGuid().ToString("N");
|
||||
|
||||
@@ -49,4 +49,5 @@ public class FirstQuestionAttemptRequest
|
||||
public string ChoiceId { get; set; } = "";
|
||||
public List<string> PresentedChoiceIds { get; set; } = [];
|
||||
public int DurationSeconds { get; set; }
|
||||
public bool MetTimeLimit { get; set; } = true;
|
||||
}
|
||||
|
||||
+4
-5
@@ -13,14 +13,13 @@ config_version=5
|
||||
config/name="RanaZe: Step 3 Trainer"
|
||||
config/version="pre-alpha"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||
config/icon="res://assets/branding/ranaze-app-icon.png"
|
||||
config/use_custom_user_dir=true
|
||||
config/custom_user_dir_name="MedMind"
|
||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||
boot_splash/bg_color=Color(0.019607844, 0.02745098, 0.05882353, 1)
|
||||
boot_splash/stretch_mode=0
|
||||
boot_splash/image="res://assets/branding/ranaze-mark.png"
|
||||
boot_splash/fullsize=false
|
||||
boot_splash/use_filter=true
|
||||
config/icon="res://assets/branding/ranaze-app-icon.png"
|
||||
|
||||
[autoload]
|
||||
|
||||
@@ -58,9 +57,9 @@ window/stretch/aspect="expand"
|
||||
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
rendering_device/driver.windows="d3d12"
|
||||
textures/vram_compression/import_etc2_astc=true
|
||||
|
||||
[step3]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi323du7o7dn2
|
||||
@@ -257,10 +257,10 @@ func clear_questions() -> void:
|
||||
ratings_changed.emit()
|
||||
|
||||
|
||||
func record_first_attempt(question_id: String, choice_id: String, presented_choice_ids: Array = [], duration_seconds: int = 0) -> void:
|
||||
func record_first_attempt(question_id: String, choice_id: String, presented_choice_ids: Array = [], duration_seconds: int = 0, met_time_limit: bool = true) -> void:
|
||||
if not AuthState.can_send_authenticated_request() or question_id == "" or choice_id == "":
|
||||
return
|
||||
_enqueue_attempt("answer", question_id, choice_id, presented_choice_ids, duration_seconds)
|
||||
_enqueue_attempt("answer", question_id, choice_id, presented_choice_ids, duration_seconds, met_time_limit)
|
||||
|
||||
|
||||
func skip_question(question_id: String) -> void:
|
||||
@@ -386,7 +386,7 @@ func _on_retry_timer_timeout() -> void:
|
||||
load_questions_from_api(false)
|
||||
|
||||
|
||||
func _enqueue_attempt(kind: String, question_id: String, choice_id: String, presented_choice_ids: Array = [], duration_seconds: int = 0) -> void:
|
||||
func _enqueue_attempt(kind: String, question_id: String, choice_id: String, presented_choice_ids: Array = [], duration_seconds: int = 0, met_time_limit: bool = true) -> void:
|
||||
var account := AuthState.username.strip_edges()
|
||||
if account == "":
|
||||
return
|
||||
@@ -400,6 +400,7 @@ func _enqueue_attempt(kind: String, question_id: String, choice_id: String, pres
|
||||
"choice_id": choice_id,
|
||||
"presented_choice_ids": presented_choice_ids.duplicate(),
|
||||
"duration_seconds": maxi(0, duration_seconds),
|
||||
"met_time_limit": met_time_limit,
|
||||
"retry_count": 0
|
||||
})
|
||||
_save_attempt_queue()
|
||||
@@ -432,7 +433,8 @@ func _process_attempt_queue() -> void:
|
||||
"questionExternalId": question_id,
|
||||
"choiceId": str(queued.get("choice_id", "")),
|
||||
"presentedChoiceIds": queued.get("presented_choice_ids", []),
|
||||
"durationSeconds": int(queued.get("duration_seconds", 0))
|
||||
"durationSeconds": int(queued.get("duration_seconds", 0)),
|
||||
"metTimeLimit": bool(queued.get("met_time_limit", true))
|
||||
})
|
||||
if str(queued.get("kind", "answer")) == "skip":
|
||||
url = "%s/%s/skip" % [question_api_base_url.rstrip("/"), question_id.uri_encode()]
|
||||
|
||||
@@ -8,6 +8,8 @@ var icon_panel: PanelContainer
|
||||
var icon_texture: TextureRect
|
||||
var value_label: Label
|
||||
var caption_label: Label
|
||||
var displayed_value := 0
|
||||
var _initialized := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -17,6 +19,7 @@ func _ready() -> void:
|
||||
refresh_theme_controls()
|
||||
_on_credits_changed()
|
||||
CreditsService.credits_changed.connect(_on_credits_changed)
|
||||
resized.connect(func(): pivot_offset = size * 0.5)
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
@@ -50,7 +53,7 @@ func _build_ui() -> void:
|
||||
value_label.add_theme_font_size_override("font_size", 15)
|
||||
copy.add_child(value_label)
|
||||
caption_label = Label.new()
|
||||
caption_label.text = "RZ Points"
|
||||
caption_label.text = "Rz Coins"
|
||||
caption_label.add_theme_font_override("font", Brand.font)
|
||||
caption_label.add_theme_font_size_override("font_size", 10)
|
||||
copy.add_child(caption_label)
|
||||
@@ -75,8 +78,30 @@ func refresh_theme_controls() -> void:
|
||||
caption_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
|
||||
|
||||
func bump() -> void:
|
||||
var tween := create_tween()
|
||||
tween.tween_property(self, "scale", Vector2(1.12, 1.12), 0.12).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(self, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
|
||||
|
||||
func _on_credits_changed() -> void:
|
||||
value_label.text = _format_thousands(CreditsService.credits) if CreditsService.loaded else "--"
|
||||
if not CreditsService.loaded:
|
||||
_initialized = false
|
||||
displayed_value = 0
|
||||
value_label.text = "--"
|
||||
return
|
||||
if not _initialized:
|
||||
_initialized = true
|
||||
displayed_value = CreditsService.credits
|
||||
value_label.text = _format_thousands(displayed_value)
|
||||
return
|
||||
var tween := create_tween()
|
||||
tween.tween_method(_set_displayed_value, displayed_value, CreditsService.credits, 0.5).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
func _set_displayed_value(value: int) -> void:
|
||||
displayed_value = value
|
||||
value_label.text = _format_thousands(value)
|
||||
|
||||
|
||||
func _format_thousands(value: int) -> String:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://doisoxqnk1mf3
|
||||
@@ -15,6 +15,7 @@ const FLAG_DARK_ICON := preload("res://assets/icons/medmind/flag-dark.svg")
|
||||
const FLAG_LIGHT_ICON := preload("res://assets/icons/medmind/flag-light.svg")
|
||||
const BADGE_CIRCLE_OUTLINE_ICON := preload("res://assets/icons/medmind/badge-circle-outline.svg")
|
||||
const BADGE_CIRCLE_FILLED_ICON := preload("res://assets/icons/medmind/badge-circle-filled.svg")
|
||||
const COIN_ICON := preload("res://assets/icons/lucide/star.svg")
|
||||
const FEEDBACK_FADE_SECONDS := 0.45
|
||||
const CORRECT_ADVANCE_DELAY_SECONDS := 1.25
|
||||
const TEXT_SCALE_MIN := 0.85
|
||||
@@ -1565,12 +1566,15 @@ func _submit_or_next() -> void:
|
||||
return
|
||||
if BlockSessionService.session_kind in ["core", "bookmark", "review"]:
|
||||
var attempted_choice := selected_choice
|
||||
var met_time_limit := true
|
||||
if BlockSessionService.session_kind == "core":
|
||||
met_time_limit = BlockSessionService.get_current_elapsed_seconds() <= BlockSessionService.core_current_target_seconds
|
||||
QuestionBankService.record_first_attempt(
|
||||
question_id_for_attempt,
|
||||
attempted_choice,
|
||||
_presented_choice_ids(),
|
||||
int(floor(BlockSessionService.get_core_question_total_elapsed_seconds()))
|
||||
int(floor(BlockSessionService.get_core_question_total_elapsed_seconds())),
|
||||
met_time_limit
|
||||
)
|
||||
var result: Dictionary
|
||||
if BlockSessionService.session_kind == "core":
|
||||
@@ -1593,6 +1597,15 @@ func _submit_or_next() -> void:
|
||||
_update_question_elo_label(true)
|
||||
_show_answer_review()
|
||||
_trigger_correct_feedback()
|
||||
var earned_coin := (
|
||||
BlockSessionService.session_kind == "core"
|
||||
and bool(result.get("mastered", false))
|
||||
and met_time_limit
|
||||
)
|
||||
if earned_coin:
|
||||
var correct_choice_index := _choice_index_for_id(selected_choice)
|
||||
if correct_choice_index >= 0 and correct_choice_index < choice_rows.size():
|
||||
_play_coin_burst(choice_rows[correct_choice_index].get_global_rect())
|
||||
return
|
||||
_lock_wrong_choice(attempted_choice)
|
||||
_show_wrong_answer_followup(attempted_choice)
|
||||
@@ -2185,6 +2198,39 @@ func _trigger_correct_feedback() -> void:
|
||||
feedback_fog.color = Color(0.25, 1.0, 0.25, _feedback_overlay_alpha(true))
|
||||
|
||||
|
||||
func _play_coin_burst(origin_rect: Rect2) -> void:
|
||||
if credits_badge == null:
|
||||
return
|
||||
var origin := origin_rect.get_center()
|
||||
var target := credits_badge.get_global_rect().get_center()
|
||||
var coin_count := 6
|
||||
var flight_time := 0.55
|
||||
var stagger := 0.035
|
||||
for index in range(coin_count):
|
||||
var coin := TextureRect.new()
|
||||
coin.texture = COIN_ICON
|
||||
coin.modulate = Brand.AMBER
|
||||
coin.custom_minimum_size = Vector2(16, 16)
|
||||
coin.size = Vector2(16, 16)
|
||||
coin.top_level = true
|
||||
coin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
coin.pivot_offset = Vector2(8, 8)
|
||||
var jitter := Vector2(randf_range(-24.0, 24.0), randf_range(-16.0, 16.0))
|
||||
coin.global_position = origin + jitter - coin.size * 0.5
|
||||
add_child(coin)
|
||||
var tween := create_tween()
|
||||
tween.tween_interval(index * stagger)
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(coin, "global_position", target - coin.size * 0.5, flight_time).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
|
||||
tween.tween_property(coin, "scale", Vector2(0.6, 0.6), flight_time).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
|
||||
tween.set_parallel(false)
|
||||
tween.tween_callback(coin.queue_free)
|
||||
var settle_time := (coin_count - 1) * stagger + flight_time
|
||||
get_tree().create_timer(settle_time).timeout.connect(func():
|
||||
if credits_badge != null:
|
||||
credits_badge.bump())
|
||||
|
||||
|
||||
func _update_answer_feedback(delta: float) -> void:
|
||||
if shake_time > 0.0:
|
||||
shake_time = max(0.0, shake_time - delta)
|
||||
|
||||
Reference in New Issue
Block a user