initial commit

This commit is contained in:
mr0xb 2025-12-27 00:20:33 -05:00
commit 94847a50a2
42 changed files with 2063 additions and 0 deletions

117
scripts/models/deck.gd Normal file
View file

@ -0,0 +1,117 @@
class_name Deck
extends Resource
@export var id: String = ""
@export var name: String = "New Deck"
@export var description: String = ""
@export var cards: Array[Flashcard] = []
@export var author_id: String = "local"
@export var author_name: String = "Local User"
@export var author_created_at: String = ""
@export var created_at: String = ""
@export var updated_at: String = ""
@export var color_theme: Color = Color.CORNFLOWER_BLUE
@export var icon: String = ""
@export var tags: Array[String] = []
@export var is_public: bool = false
@export var download_count: int = 0
@export var rating: float = 0.0
func _init() -> void:
if id == "":
id = _generate_uuid()
var now = Time.get_datetime_string_from_system(true, true) + "Z"
created_at = now
updated_at = now
author_created_at = now
func to_dict() -> Dictionary:
var cards_data: Array = []
for card in cards:
cards_data.append(card.to_dict())
return {
"version": "1.0",
"deck": {
"id": id,
"name": name,
"description": description,
"author": {
"id": author_id,
"name": author_name,
"created_at": author_created_at
},
"metadata": {
"created_at": created_at,
"updated_at": updated_at,
"color_theme": color_theme.to_html(),
"icon": icon,
"tags": tags.duplicate(),
"is_public": is_public,
"download_count": download_count,
"rating": rating
},
"cards": cards_data
}
}
func from_dict(data: Dictionary) -> void:
var deck_data = data.get("deck", data)
id = deck_data.get("id", _generate_uuid())
name = deck_data.get("name", "New Deck")
description = deck_data.get("description", "")
var author = deck_data.get("author", {})
author_id = author.get("id", "local")
author_name = author.get("name", "Local User")
author_created_at = author.get("created_at", Time.get_datetime_string_from_system(true, true) + "Z")
var metadata = deck_data.get("metadata", {})
created_at = metadata.get("created_at", Time.get_datetime_string_from_system(true, true) + "Z")
updated_at = metadata.get("updated_at", created_at)
color_theme = Color.from_string(metadata.get("color_theme", "#6495ED"), Color.CORNFLOWER_BLUE)
icon = metadata.get("icon", "")
tags.assign(metadata.get("tags", []))
is_public = metadata.get("is_public", false)
download_count = metadata.get("download_count", 0)
rating = metadata.get("rating", 0.0)
cards.clear()
var cards_data = deck_data.get("cards", [])
for card_data in cards_data:
var card = Flashcard.new()
card.from_dict(card_data)
cards.append(card)
func add_card(card: Flashcard) -> void:
cards.append(card)
_update_modified_date()
func remove_card(card_id: String) -> bool:
for i in range(cards.size()):
if cards[i].id == card_id:
cards.remove_at(i)
_update_modified_date()
return true
return false
func get_card_by_id(card_id: String) -> Flashcard:
for card in cards:
if card.id == card_id:
return card
return null
func get_card_count() -> int:
return cards.size()
func _update_modified_date() -> void:
updated_at = Time.get_datetime_string_from_system(true, true) + "Z"
func _generate_uuid() -> String:
return "%x%x-%x-%x-%x-%x%x%x" % [
randi(), randi(), randi(), randi(),
randi(), randi(), randi(), randi()
]

View file

@ -0,0 +1 @@
uid://dio0iqdf45ig1

View file

@ -0,0 +1,91 @@
class_name Flashcard
extends Resource
enum QuestionType {
BASIC,
MULTIPLE_CHOICE
}
@export var id: String = ""
@export var question_text: String = ""
@export var question_image: String = ""
@export var question_color: Color = Color.WHITE
@export var question_format: String = "plain"
@export var answer_text: String = ""
@export var answer_image: String = ""
@export var answer_color: Color = Color.WHITE
@export var answer_format: String = "plain"
@export var card_type: QuestionType = QuestionType.BASIC
@export var choices: Array[Dictionary] = []
@export var tags: Array[String] = []
@export var difficulty: int = 1
@export var hint: String = ""
func _init() -> void:
if id == "":
id = _generate_uuid()
func to_dict() -> Dictionary:
return {
"id": id,
"question": {
"text": question_text,
"image": question_image,
"color": question_color.to_html(),
"format": question_format
},
"answer": {
"text": answer_text,
"image": answer_image,
"color": answer_color.to_html(),
"format": answer_format
},
"type": "basic" if card_type == QuestionType.BASIC else "multiple_choice",
"choices": choices.duplicate(),
"metadata": {
"tags": tags.duplicate(),
"difficulty": difficulty,
"hint": hint
}
}
func from_dict(data: Dictionary) -> void:
id = data.get("id", _generate_uuid())
var question = data.get("question", {})
question_text = question.get("text", "")
question_image = question.get("image", "")
question_color = Color.from_string(question.get("color", "#FFFFFF"), Color.WHITE)
question_format = question.get("format", "plain")
var answer = data.get("answer", {})
answer_text = answer.get("text", "")
answer_image = answer.get("image", "")
answer_color = Color.from_string(answer.get("color", "#FFFFFF"), Color.WHITE)
answer_format = answer.get("format", "plain")
var type_str = data.get("type", "basic")
card_type = QuestionType.MULTIPLE_CHOICE if type_str == "multiple_choice" else QuestionType.BASIC
choices.assign(data.get("choices", []))
var metadata = data.get("metadata", {})
tags.assign(metadata.get("tags", []))
difficulty = metadata.get("difficulty", 1)
hint = metadata.get("hint", "")
func get_correct_choice_index() -> int:
for i in range(choices.size()):
if choices[i].get("is_correct", false):
return i
return -1
func _generate_uuid() -> String:
return "%x%x-%x-%x-%x-%x%x%x" % [
randi(), randi(), randi(), randi(),
randi(), randi(), randi(), randi()
]

View file

@ -0,0 +1 @@
uid://bhpp4v2b5cqxq

116
scripts/models/progress.gd Normal file
View file

@ -0,0 +1,116 @@
class_name Progress
extends Resource
@export var deck_id: String = ""
@export var total_sessions: int = 0
@export var total_cards_studied: int = 0
@export var total_correct: int = 0
@export var total_incorrect: int = 0
@export var current_streak: int = 0
@export var best_streak: int = 0
@export var last_session_at: String = ""
@export var card_stats: Dictionary = {}
func to_dict() -> Dictionary:
return {
"deck_id": deck_id,
"stats": {
"total_sessions": total_sessions,
"total_cards_studied": total_cards_studied,
"total_correct": total_correct,
"total_incorrect": total_incorrect,
"current_streak": current_streak,
"best_streak": best_streak,
"last_session_at": last_session_at
},
"card_stats": card_stats.duplicate()
}
func from_dict(data: Dictionary) -> void:
deck_id = data.get("deck_id", "")
var stats = data.get("stats", {})
total_sessions = stats.get("total_sessions", 0)
total_cards_studied = stats.get("total_cards_studied", 0)
total_correct = stats.get("total_correct", 0)
total_incorrect = stats.get("total_incorrect", 0)
current_streak = stats.get("current_streak", 0)
best_streak = stats.get("best_streak", 0)
last_session_at = stats.get("last_session_at", "")
card_stats = data.get("card_stats", {}).duplicate()
func record_answer(card_id: String, is_correct: bool) -> void:
if not card_stats.has(card_id):
card_stats[card_id] = {
"correct": 0,
"incorrect": 0,
"last_seen_at": "",
"next_review_at": "",
"ease_factor": 2.5
}
if is_correct:
total_correct += 1
card_stats[card_id]["correct"] += 1
current_streak += 1
if current_streak > best_streak:
best_streak = current_streak
else:
total_incorrect += 1
card_stats[card_id]["incorrect"] += 1
current_streak = 0
total_cards_studied += 1
var now = Time.get_datetime_string_from_system(true, true) + "Z"
card_stats[card_id]["last_seen_at"] = now
last_session_at = now
_update_next_review(card_id, is_correct)
func get_accuracy() -> float:
var total = total_correct + total_incorrect
if total == 0:
return 0.0
return (float(total_correct) / float(total)) * 100.0
func get_card_stats(card_id: String) -> Dictionary:
return card_stats.get(card_id, {
"correct": 0,
"incorrect": 0,
"last_seen_at": "",
"next_review_at": "",
"ease_factor": 2.5
})
func _update_next_review(card_id: String, is_correct: bool) -> void:
var stats = card_stats[card_id]
var ease_factor: float = stats.get("ease_factor", 2.5)
if is_correct:
ease_factor = min(ease_factor + 0.1, 3.0)
else:
ease_factor = max(ease_factor - 0.2, 1.3)
stats["ease_factor"] = ease_factor
var review_interval_days: int = 1
if is_correct:
var correct_count = stats.get("correct", 0)
review_interval_days = int(correct_count * ease_factor)
else:
review_interval_days = 1
var now_unix = Time.get_unix_time_from_system()
#var now_unix = Time.get_time_dict_from_unix_time(Time.get_unix_time_from_system())
var next_review_unix = now_unix + (review_interval_days * 86400) # 86400 seconds in a day
var next_review_dict = Time.get_datetime_dict_from_unix_time(int(next_review_unix))
stats["next_review_at"] = "%04d-%02d-%02dT%02d:%02d:%02dZ" % [
next_review_dict.year, next_review_dict.month, next_review_dict.day,
next_review_dict.hour, next_review_dict.minute, next_review_dict.second
]
func start_session() -> void:
total_sessions += 1

View file

@ -0,0 +1 @@
uid://ccov8lpi2n3be

View file

@ -0,0 +1,112 @@
class_name QuizSession
extends Resource
signal question_answered(is_correct: bool, card: Flashcard)
signal session_completed(results: Dictionary)
signal progress_updated(current: int, total: int)
@export var deck: Deck
@export var current_index: int = 0
@export var shuffled_cards: Array[Flashcard] = []
@export var session_correct: int = 0
@export var session_incorrect: int = 0
@export var start_time: float = 0.0
@export var is_active: bool = false
func start_session(deck_to_study: Deck, shuffle: bool = true) -> void:
if not deck_to_study or deck_to_study.cards.is_empty():
push_error("Cannot start session with empty or null deck")
return
deck = deck_to_study
current_index = 0
session_correct = 0
session_incorrect = 0
start_time = Time.get_ticks_msec() / 1000.0
is_active = true
shuffled_cards.clear()
for card in deck.cards:
shuffled_cards.append(card)
if shuffle:
shuffled_cards.shuffle()
progress_updated.emit(current_index, shuffled_cards.size())
func get_current_card() -> Flashcard:
if current_index < shuffled_cards.size():
return shuffled_cards[current_index]
return null
func answer_question(is_correct: bool) -> void:
if not is_active:
push_warning("Cannot answer question - session is not active")
return
var current_card = get_current_card()
if not current_card:
push_warning("No current card to answer")
return
if is_correct:
session_correct += 1
else:
session_incorrect += 1
question_answered.emit(is_correct, current_card)
current_index += 1
progress_updated.emit(current_index, shuffled_cards.size())
if current_index >= shuffled_cards.size():
complete_session()
func complete_session() -> void:
if not is_active:
return
is_active = false
var end_time = Time.get_ticks_msec() / 1000.0
var duration = end_time - start_time
var results = {
"total_questions": shuffled_cards.size(),
"correct": session_correct,
"incorrect": session_incorrect,
"accuracy": get_session_accuracy(),
"duration_seconds": duration,
"deck_id": deck.id if deck else "",
"deck_name": deck.name if deck else ""
}
session_completed.emit(results)
func get_session_accuracy() -> float:
var total = session_correct + session_incorrect
if total == 0:
return 0.0
return (float(session_correct) / float(total)) * 100.0
func get_progress_percentage() -> float:
if shuffled_cards.size() == 0:
return 0.0
return (float(current_index) / float(shuffled_cards.size())) * 100.0
func get_remaining_cards() -> int:
return shuffled_cards.size() - current_index
func has_more_cards() -> bool:
return current_index < shuffled_cards.size()
func reset_session(shuffle_again: bool = true) -> void:
current_index = 0
session_correct = 0
session_incorrect = 0
start_time = Time.get_ticks_msec() / 1000.0
is_active = true
if shuffle_again:
shuffled_cards.shuffle()
progress_updated.emit(current_index, shuffled_cards.size())

View file

@ -0,0 +1 @@
uid://b1j31h2s1bwyq