Track real first attempt question stats
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using QuestionsApi.Models;
|
using QuestionsApi.Models;
|
||||||
using QuestionsApi.Services;
|
using QuestionsApi.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace QuestionsApi.Controllers;
|
namespace QuestionsApi.Controllers;
|
||||||
|
|
||||||
@@ -11,10 +12,12 @@ namespace QuestionsApi.Controllers;
|
|||||||
public class QuestionsController : ControllerBase
|
public class QuestionsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly QuestionStore _questions;
|
private readonly QuestionStore _questions;
|
||||||
|
private readonly QuestionAttemptStore _attempts;
|
||||||
|
|
||||||
public QuestionsController(QuestionStore questions)
|
public QuestionsController(QuestionStore questions, QuestionAttemptStore attempts)
|
||||||
{
|
{
|
||||||
_questions = questions;
|
_questions = questions;
|
||||||
|
_attempts = attempts;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("systems")]
|
[HttpGet("systems")]
|
||||||
@@ -30,7 +33,12 @@ public class QuestionsController : ControllerBase
|
|||||||
public async Task<IActionResult> Search([FromBody] QuestionSearchRequest req)
|
public async Task<IActionResult> Search([FromBody] QuestionSearchRequest req)
|
||||||
{
|
{
|
||||||
var questions = await _questions.SearchAsync(req, User.IsInRole("SUPER"));
|
var questions = await _questions.SearchAsync(req, User.IsInRole("SUPER"));
|
||||||
return Ok(questions.Select(QuestionResponse.FromModel).ToList());
|
var percentages = await _attempts.GetPercentagesByQuestionAsync(questions.Select(q => q.ExternalId));
|
||||||
|
return Ok(questions.Select(q =>
|
||||||
|
{
|
||||||
|
percentages.TryGetValue(q.ExternalId, out var questionPercentages);
|
||||||
|
return QuestionResponse.FromModel(q, questionPercentages);
|
||||||
|
}).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@@ -41,7 +49,43 @@ public class QuestionsController : ControllerBase
|
|||||||
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
|
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
return Ok(QuestionResponse.FromModel(question));
|
var percentages = await _attempts.GetPercentagesForQuestionAsync(question.ExternalId);
|
||||||
|
return Ok(QuestionResponse.FromModel(question, percentages));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("attempts/first")]
|
||||||
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
public async Task<IActionResult> RecordFirstAttempt([FromBody] FirstQuestionAttemptRequest req)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(req.QuestionExternalId))
|
||||||
|
return BadRequest("questionExternalId required");
|
||||||
|
if (string.IsNullOrWhiteSpace(req.ChoiceId))
|
||||||
|
return BadRequest("choiceId required");
|
||||||
|
|
||||||
|
var question = await _questions.GetByExternalIdAsync(req.QuestionExternalId.Trim());
|
||||||
|
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
|
||||||
|
return NotFound();
|
||||||
|
if (!question.Choices.Any(c => c.Id == req.ChoiceId.Trim()))
|
||||||
|
return BadRequest("choiceId must match a choice id");
|
||||||
|
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||||
|
?? User.FindFirstValue(ClaimTypes.Name)
|
||||||
|
?? User.Identity?.Name
|
||||||
|
?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
await _attempts.RecordFirstAttemptAsync(
|
||||||
|
question.ExternalId,
|
||||||
|
userId,
|
||||||
|
req.ChoiceId.Trim(),
|
||||||
|
req.ChoiceId.Trim() == question.CorrectChoiceId);
|
||||||
|
|
||||||
|
return Ok(new FirstQuestionAttemptResponse
|
||||||
|
{
|
||||||
|
QuestionExternalId = question.ExternalId,
|
||||||
|
ChoiceSelectionPercentages = await _attempts.GetPercentagesForQuestionAsync(question.ExternalId)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace QuestionsApi.Models;
|
||||||
|
|
||||||
|
public class QuestionAttempt
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string QuestionExternalId { get; set; } = "";
|
||||||
|
public string UserId { get; set; } = "";
|
||||||
|
public string ChoiceId { get; set; } = "";
|
||||||
|
public bool IsCorrect { get; set; }
|
||||||
|
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -28,3 +28,9 @@ public class UpsertQuestionRequest
|
|||||||
public List<string> Tags { get; set; } = [];
|
public List<string> Tags { get; set; } = [];
|
||||||
public bool IsPublished { get; set; } = true;
|
public bool IsPublished { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FirstQuestionAttemptRequest
|
||||||
|
{
|
||||||
|
public string QuestionExternalId { get; set; } = "";
|
||||||
|
public string ChoiceId { get; set; } = "";
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class QuestionResponse
|
|||||||
public List<string> Media { get; set; } = [];
|
public List<string> Media { get; set; } = [];
|
||||||
public List<string> Tags { get; set; } = [];
|
public List<string> Tags { get; set; } = [];
|
||||||
|
|
||||||
public static QuestionResponse FromModel(Question question) => new()
|
public static QuestionResponse FromModel(Question question, Dictionary<string, int>? liveChoiceSelectionPercentages = null) => new()
|
||||||
{
|
{
|
||||||
Id = question.Id ?? "",
|
Id = question.Id ?? "",
|
||||||
ExternalId = question.ExternalId,
|
ExternalId = question.ExternalId,
|
||||||
@@ -28,7 +28,9 @@ public class QuestionResponse
|
|||||||
CorrectChoiceId = question.CorrectChoiceId,
|
CorrectChoiceId = question.CorrectChoiceId,
|
||||||
Explanation = question.Explanation,
|
Explanation = question.Explanation,
|
||||||
ChoiceExplanations = question.ChoiceExplanations,
|
ChoiceExplanations = question.ChoiceExplanations,
|
||||||
ChoiceSelectionPercentages = question.ChoiceSelectionPercentages,
|
ChoiceSelectionPercentages = liveChoiceSelectionPercentages is { Count: > 0 }
|
||||||
|
? liveChoiceSelectionPercentages
|
||||||
|
: question.ChoiceSelectionPercentages,
|
||||||
Discipline = question.Discipline,
|
Discipline = question.Discipline,
|
||||||
System = question.System,
|
System = question.System,
|
||||||
Difficulty = question.Difficulty,
|
Difficulty = question.Difficulty,
|
||||||
@@ -37,3 +39,9 @@ public class QuestionResponse
|
|||||||
Tags = question.Tags
|
Tags = question.Tags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FirstQuestionAttemptResponse
|
||||||
|
{
|
||||||
|
public string QuestionExternalId { get; set; } = "";
|
||||||
|
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
builder.Services.AddSingleton<QuestionStore>();
|
builder.Services.AddSingleton<QuestionStore>();
|
||||||
|
builder.Services.AddSingleton<QuestionAttemptStore>();
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
builder.Services.AddSwaggerGen(c =>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using QuestionsApi.Models;
|
||||||
|
|
||||||
|
namespace QuestionsApi.Services;
|
||||||
|
|
||||||
|
public class QuestionAttemptStore
|
||||||
|
{
|
||||||
|
private readonly IMongoCollection<QuestionAttempt> _col;
|
||||||
|
|
||||||
|
public QuestionAttemptStore(IConfiguration cfg)
|
||||||
|
{
|
||||||
|
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||||
|
var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb";
|
||||||
|
var client = new MongoClient(cs);
|
||||||
|
var db = client.GetDatabase(dbName);
|
||||||
|
_col = db.GetCollection<QuestionAttempt>("QuestionAttempts");
|
||||||
|
|
||||||
|
var firstAttemptIndex = Builders<QuestionAttempt>.IndexKeys
|
||||||
|
.Ascending(a => a.QuestionExternalId)
|
||||||
|
.Ascending(a => a.UserId);
|
||||||
|
_col.Indexes.CreateOne(new CreateIndexModel<QuestionAttempt>(
|
||||||
|
firstAttemptIndex,
|
||||||
|
new CreateIndexOptions { Unique = true }));
|
||||||
|
|
||||||
|
var questionIndex = Builders<QuestionAttempt>.IndexKeys.Ascending(a => a.QuestionExternalId);
|
||||||
|
_col.Indexes.CreateOne(new CreateIndexModel<QuestionAttempt>(questionIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RecordFirstAttemptAsync(string questionExternalId, string userId, string choiceId, bool isCorrect)
|
||||||
|
{
|
||||||
|
var attempt = new QuestionAttempt
|
||||||
|
{
|
||||||
|
QuestionExternalId = questionExternalId,
|
||||||
|
UserId = userId,
|
||||||
|
ChoiceId = choiceId,
|
||||||
|
IsCorrect = isCorrect,
|
||||||
|
CreatedUtc = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _col.InsertOneAsync(attempt);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, int>> GetPercentagesForQuestionAsync(string questionExternalId)
|
||||||
|
{
|
||||||
|
var results = await GetPercentagesByQuestionAsync([questionExternalId]);
|
||||||
|
return results.GetValueOrDefault(questionExternalId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, Dictionary<string, int>>> GetPercentagesByQuestionAsync(IEnumerable<string> questionExternalIds)
|
||||||
|
{
|
||||||
|
var ids = questionExternalIds.Where(id => !string.IsNullOrWhiteSpace(id)).Distinct().ToList();
|
||||||
|
if (ids.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var attempts = await _col.Find(a => ids.Contains(a.QuestionExternalId)).ToListAsync();
|
||||||
|
var output = new Dictionary<string, Dictionary<string, int>>();
|
||||||
|
|
||||||
|
foreach (var group in attempts.GroupBy(a => a.QuestionExternalId))
|
||||||
|
{
|
||||||
|
var total = group.Count();
|
||||||
|
if (total == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
output[group.Key] = group
|
||||||
|
.GroupBy(a => a.ChoiceId)
|
||||||
|
.ToDictionary(
|
||||||
|
choiceGroup => choiceGroup.Key,
|
||||||
|
choiceGroup => (int)Math.Round(choiceGroup.Count() * 100.0 / total));
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,9 @@ public class QuestionStore
|
|||||||
public async Task<Question?> GetByIdAsync(string id) =>
|
public async Task<Question?> GetByIdAsync(string id) =>
|
||||||
await _col.Find(q => q.Id == id).FirstOrDefaultAsync();
|
await _col.Find(q => q.Id == id).FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
public async Task<Question?> GetByExternalIdAsync(string externalId) =>
|
||||||
|
await _col.Find(q => q.ExternalId == externalId).FirstOrDefaultAsync();
|
||||||
|
|
||||||
public async Task<Question?> CreateAsync(UpsertQuestionRequest req)
|
public async Task<Question?> CreateAsync(UpsertQuestionRequest req)
|
||||||
{
|
{
|
||||||
var question = FromRequest(req);
|
var question = FromRequest(req);
|
||||||
|
|||||||
@@ -198,6 +198,15 @@ func get_answer_for_index(index: int) -> Variant:
|
|||||||
return answers[index]
|
return answers[index]
|
||||||
|
|
||||||
|
|
||||||
|
func update_question_choice_percentages(question_id: String, percentages: Dictionary) -> void:
|
||||||
|
for index in range(active_questions.size()):
|
||||||
|
var question: Dictionary = active_questions[index]
|
||||||
|
if str(question.get("id", "")) == question_id:
|
||||||
|
question["choice_selection_percentages"] = percentages
|
||||||
|
active_questions[index] = question
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
func advance_core_question() -> bool:
|
func advance_core_question() -> bool:
|
||||||
var question := _draw_core_question()
|
var question := _draw_core_question()
|
||||||
if question.is_empty():
|
if question.is_empty():
|
||||||
|
|||||||
@@ -123,6 +123,16 @@ func reset_profile() -> void:
|
|||||||
profile_changed.emit()
|
profile_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func reset_qbank_memory() -> void:
|
||||||
|
_ensure_tracking_fields()
|
||||||
|
profile["completed_question_ids"] = []
|
||||||
|
profile["correct_question_ids"] = []
|
||||||
|
profile["question_wrong_counts"] = {}
|
||||||
|
profile["missed_question_ids"] = []
|
||||||
|
save_profile()
|
||||||
|
profile_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
func apply_question_results(results: Dictionary) -> void:
|
func apply_question_results(results: Dictionary) -> void:
|
||||||
_ensure_daily_current()
|
_ensure_daily_current()
|
||||||
profile["xp"] = int(profile["xp"]) + int(results.get("xp_earned", 0))
|
profile["xp"] = int(profile["xp"]) + int(results.get("xp_earned", 0))
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const DEFAULT_QUESTIONS_API_URL := "https://s3q.ranaze.com/api/questions/search"
|
const DEFAULT_QUESTIONS_API_URL := "https://s3q.ranaze.com/api/questions/search"
|
||||||
|
const DEFAULT_QUESTION_ATTEMPT_API_URL := "https://s3q.ranaze.com/api/questions/attempts/first"
|
||||||
const RETRY_DELAYS := [2, 5, 10, 20, 30]
|
const RETRY_DELAYS := [2, 5, 10, 20, 30]
|
||||||
|
|
||||||
signal questions_changed
|
signal questions_changed
|
||||||
signal questions_load_failed(message: String)
|
signal questions_load_failed(message: String)
|
||||||
|
signal choice_percentages_updated(question_id: String, percentages: Dictionary)
|
||||||
|
|
||||||
var questions: Array = []
|
var questions: Array = []
|
||||||
var request: HTTPRequest
|
var request: HTTPRequest
|
||||||
|
var attempt_request: HTTPRequest
|
||||||
var retry_timer: Timer
|
var retry_timer: Timer
|
||||||
var questions_api_url := DEFAULT_QUESTIONS_API_URL
|
var questions_api_url := DEFAULT_QUESTIONS_API_URL
|
||||||
|
var question_attempt_api_url := DEFAULT_QUESTION_ATTEMPT_API_URL
|
||||||
var is_loading := false
|
var is_loading := false
|
||||||
var last_error := ""
|
var last_error := ""
|
||||||
var load_state := "idle"
|
var load_state := "idle"
|
||||||
@@ -18,10 +22,15 @@ var next_retry_seconds := 0
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
questions_api_url = str(ProjectSettings.get_setting("step3/questions_api_url", DEFAULT_QUESTIONS_API_URL))
|
questions_api_url = str(ProjectSettings.get_setting("step3/questions_api_url", DEFAULT_QUESTIONS_API_URL))
|
||||||
|
question_attempt_api_url = str(ProjectSettings.get_setting("step3/question_attempt_api_url", DEFAULT_QUESTION_ATTEMPT_API_URL))
|
||||||
request = HTTPRequest.new()
|
request = HTTPRequest.new()
|
||||||
request.request_completed.connect(_on_api_questions_loaded)
|
request.request_completed.connect(_on_api_questions_loaded)
|
||||||
add_child(request)
|
add_child(request)
|
||||||
|
|
||||||
|
attempt_request = HTTPRequest.new()
|
||||||
|
attempt_request.request_completed.connect(_on_first_attempt_recorded)
|
||||||
|
add_child(attempt_request)
|
||||||
|
|
||||||
retry_timer = Timer.new()
|
retry_timer = Timer.new()
|
||||||
retry_timer.one_shot = true
|
retry_timer.one_shot = true
|
||||||
retry_timer.timeout.connect(_on_retry_timer_timeout)
|
retry_timer.timeout.connect(_on_retry_timer_timeout)
|
||||||
@@ -96,6 +105,8 @@ func retry_now() -> void:
|
|||||||
func clear_questions() -> void:
|
func clear_questions() -> void:
|
||||||
if request != null:
|
if request != null:
|
||||||
request.cancel_request()
|
request.cancel_request()
|
||||||
|
if attempt_request != null:
|
||||||
|
attempt_request.cancel_request()
|
||||||
if retry_timer != null:
|
if retry_timer != null:
|
||||||
retry_timer.stop()
|
retry_timer.stop()
|
||||||
questions.clear()
|
questions.clear()
|
||||||
@@ -107,6 +118,23 @@ func clear_questions() -> void:
|
|||||||
questions_changed.emit()
|
questions_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func record_first_attempt(question_id: String, choice_id: String) -> void:
|
||||||
|
if AuthState.access_token.is_empty() or question_id == "" or choice_id == "":
|
||||||
|
return
|
||||||
|
|
||||||
|
var headers := PackedStringArray([
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"Authorization: Bearer %s" % AuthState.access_token
|
||||||
|
])
|
||||||
|
var payload := {
|
||||||
|
"questionExternalId": question_id,
|
||||||
|
"choiceId": choice_id
|
||||||
|
}
|
||||||
|
var err := attempt_request.request(question_attempt_api_url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||||
|
if err != OK:
|
||||||
|
push_warning("Failed to record first question attempt.")
|
||||||
|
|
||||||
|
|
||||||
func get_all_questions() -> Array:
|
func get_all_questions() -> Array:
|
||||||
return questions.duplicate(true)
|
return questions.duplicate(true)
|
||||||
|
|
||||||
@@ -170,6 +198,34 @@ func _on_retry_timer_timeout() -> void:
|
|||||||
load_questions_from_api(false)
|
load_questions_from_api(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_first_attempt_recorded(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
|
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
push_warning("Question attempt API request failed with status %d." % response_code)
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
push_warning("Question attempt API response was not an object.")
|
||||||
|
return
|
||||||
|
|
||||||
|
var question_id := str(parsed.get("questionExternalId", parsed.get("question_external_id", "")))
|
||||||
|
var percentages: Dictionary = parsed.get("choiceSelectionPercentages", parsed.get("choice_selection_percentages", {}))
|
||||||
|
if question_id == "":
|
||||||
|
return
|
||||||
|
|
||||||
|
_update_question_percentages(question_id, percentages)
|
||||||
|
choice_percentages_updated.emit(question_id, percentages)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_question_percentages(question_id: String, percentages: Dictionary) -> void:
|
||||||
|
for index in range(questions.size()):
|
||||||
|
var question: Dictionary = questions[index]
|
||||||
|
if str(question.get("id", "")) == question_id:
|
||||||
|
question["choice_selection_percentages"] = percentages
|
||||||
|
questions[index] = question
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
func _is_valid_question(item: Variant) -> bool:
|
func _is_valid_question(item: Variant) -> bool:
|
||||||
if typeof(item) != TYPE_DICTIONARY:
|
if typeof(item) != TYPE_DICTIONARY:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -192,6 +192,13 @@ func _build_app_menu() -> void:
|
|||||||
resume.pressed.connect(_close_app_menu)
|
resume.pressed.connect(_close_app_menu)
|
||||||
content.add_child(resume)
|
content.add_child(resume)
|
||||||
|
|
||||||
|
var profile := Button.new()
|
||||||
|
profile.text = "Profile"
|
||||||
|
profile.custom_minimum_size = Vector2(0, 48)
|
||||||
|
Style.style_chip(profile, Style.CYAN)
|
||||||
|
profile.pressed.connect(_open_profile_from_menu)
|
||||||
|
content.add_child(profile)
|
||||||
|
|
||||||
menu_exit_button = Button.new()
|
menu_exit_button = Button.new()
|
||||||
menu_exit_button.text = "Exit Run"
|
menu_exit_button.text = "Exit Run"
|
||||||
menu_exit_button.custom_minimum_size = Vector2(0, 48)
|
menu_exit_button.custom_minimum_size = Vector2(0, 48)
|
||||||
@@ -232,3 +239,10 @@ func _exit_run() -> void:
|
|||||||
if BlockSessionService.block_started:
|
if BlockSessionService.block_started:
|
||||||
BlockSessionService.finish_block()
|
BlockSessionService.finish_block()
|
||||||
show_screen("dashboard")
|
show_screen("dashboard")
|
||||||
|
|
||||||
|
|
||||||
|
func _open_profile_from_menu() -> void:
|
||||||
|
_close_app_menu()
|
||||||
|
if BlockSessionService.block_started:
|
||||||
|
BlockSessionService.finish_block()
|
||||||
|
show_screen("profile")
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const Style := preload("res://scripts/ui/ui_style.gd")
|
|||||||
|
|
||||||
var main: Control
|
var main: Control
|
||||||
var profile_text: RichTextLabel
|
var profile_text: RichTextLabel
|
||||||
|
var reset_qbank_button: Button
|
||||||
|
var reset_qbank_pending := false
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
@@ -38,10 +40,17 @@ func _ready() -> void:
|
|||||||
on_show()
|
on_show()
|
||||||
)
|
)
|
||||||
row.add_child(reset)
|
row.add_child(reset)
|
||||||
|
|
||||||
|
reset_qbank_button = Button.new()
|
||||||
|
reset_qbank_button.text = "Reset QBank"
|
||||||
|
Style.style_chip(reset_qbank_button, Style.AMBER)
|
||||||
|
reset_qbank_button.pressed.connect(_on_reset_qbank_pressed)
|
||||||
|
row.add_child(reset_qbank_button)
|
||||||
add_child(row)
|
add_child(row)
|
||||||
|
|
||||||
|
|
||||||
func on_show(_payload: Variant = null) -> void:
|
func on_show(_payload: Variant = null) -> void:
|
||||||
|
_reset_qbank_confirmation()
|
||||||
var profile := ProfileService.get_profile()
|
var profile := ProfileService.get_profile()
|
||||||
var xp_into := ProgressionService.xp_into_level(int(profile["xp"]))
|
var xp_into := ProgressionService.xp_into_level(int(profile["xp"]))
|
||||||
var text := "[b]Level %d[/b]\n%d total XP (%d/%d to next level)\nCurrent streak: %d\nBest streak: %d\nToday: %d/%d questions, +%d XP\n\n[b]Badges[/b]\n" % [
|
var text := "[b]Level %d[/b]\n%d total XP (%d/%d to next level)\nCurrent streak: %d\nBest streak: %d\nToday: %d/%d questions, +%d XP\n\n[b]Badges[/b]\n" % [
|
||||||
@@ -82,3 +91,24 @@ func on_show(_payload: Variant = null) -> void:
|
|||||||
block.get("xp_earned", 0)
|
block.get("xp_earned", 0)
|
||||||
]
|
]
|
||||||
profile_text.text = text
|
profile_text.text = text
|
||||||
|
|
||||||
|
|
||||||
|
func _on_reset_qbank_pressed() -> void:
|
||||||
|
if not reset_qbank_pending:
|
||||||
|
reset_qbank_pending = true
|
||||||
|
reset_qbank_button.text = "Confirm Reset QBank"
|
||||||
|
Style.style_chip(reset_qbank_button, Style.RED)
|
||||||
|
return
|
||||||
|
|
||||||
|
ProfileService.reset_qbank_memory()
|
||||||
|
if BlockSessionService.core_status_message == "All available questions cleared.":
|
||||||
|
BlockSessionService.core_status_message = ""
|
||||||
|
_reset_qbank_confirmation()
|
||||||
|
on_show()
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_qbank_confirmation() -> void:
|
||||||
|
reset_qbank_pending = false
|
||||||
|
if reset_qbank_button != null:
|
||||||
|
reset_qbank_button.text = "Reset QBank"
|
||||||
|
Style.style_chip(reset_qbank_button, Style.AMBER)
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ var shake_origin := Vector2.ZERO
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
add_theme_constant_override("separation", 10)
|
add_theme_constant_override("separation", 10)
|
||||||
|
if not QuestionBankService.choice_percentages_updated.is_connected(_on_choice_percentages_updated):
|
||||||
|
QuestionBankService.choice_percentages_updated.connect(_on_choice_percentages_updated)
|
||||||
|
|
||||||
var top := HBoxContainer.new()
|
var top := HBoxContainer.new()
|
||||||
top.add_theme_constant_override("separation", 12)
|
top.add_theme_constant_override("separation", 12)
|
||||||
@@ -322,6 +324,9 @@ func _submit_or_next() -> void:
|
|||||||
if selected_choice == "":
|
if selected_choice == "":
|
||||||
return
|
return
|
||||||
if BlockSessionService.session_kind == "core":
|
if BlockSessionService.session_kind == "core":
|
||||||
|
var attempted_choice := selected_choice
|
||||||
|
var attempted_question_id := str(BlockSessionService.get_current_question().get("id", ""))
|
||||||
|
QuestionBankService.record_first_attempt(attempted_question_id, attempted_choice)
|
||||||
var result := BlockSessionService.submit_core_attempt(selected_choice, eliminated, flagged)
|
var result := BlockSessionService.submit_core_attempt(selected_choice, eliminated, flagged)
|
||||||
if result.is_empty():
|
if result.is_empty():
|
||||||
return
|
return
|
||||||
@@ -330,7 +335,7 @@ func _submit_or_next() -> void:
|
|||||||
current_answer = answer
|
current_answer = answer
|
||||||
_trigger_correct_feedback()
|
_trigger_correct_feedback()
|
||||||
return
|
return
|
||||||
_lock_wrong_choice(selected_choice)
|
_lock_wrong_choice(attempted_choice)
|
||||||
selected_choice = ""
|
selected_choice = ""
|
||||||
current_answer = {}
|
current_answer = {}
|
||||||
next_button.text = "Submit"
|
next_button.text = "Submit"
|
||||||
@@ -615,6 +620,15 @@ func _get_choice_selection_percentage(choice_id: String) -> int:
|
|||||||
return clamp(int(percentages.get(choice_id, -1)), 0, 100)
|
return clamp(int(percentages.get(choice_id, -1)), 0, 100)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_choice_percentages_updated(question_id: String, percentages: Dictionary) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
if str(BlockSessionService.get_current_question().get("id", "")) != question_id:
|
||||||
|
return
|
||||||
|
BlockSessionService.update_question_choice_percentages(question_id, percentages)
|
||||||
|
_apply_saved_choice_state()
|
||||||
|
|
||||||
|
|
||||||
func _strike_text(value: String) -> String:
|
func _strike_text(value: String) -> String:
|
||||||
var result := ""
|
var result := ""
|
||||||
for character in value:
|
for character in value:
|
||||||
|
|||||||
Reference in New Issue
Block a user