mirror of
https://github.com/mr0xb/godot-flashcards.git
synced 2026-08-27 20:44:56 -04:00
initial commit
This commit is contained in:
commit
94847a50a2
42 changed files with 2063 additions and 0 deletions
117
scripts/models/deck.gd
Normal file
117
scripts/models/deck.gd
Normal 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()
|
||||
]
|
||||
1
scripts/models/deck.gd.uid
Normal file
1
scripts/models/deck.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dio0iqdf45ig1
|
||||
91
scripts/models/flashcard.gd
Normal file
91
scripts/models/flashcard.gd
Normal 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()
|
||||
]
|
||||
1
scripts/models/flashcard.gd.uid
Normal file
1
scripts/models/flashcard.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bhpp4v2b5cqxq
|
||||
116
scripts/models/progress.gd
Normal file
116
scripts/models/progress.gd
Normal 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
|
||||
1
scripts/models/progress.gd.uid
Normal file
1
scripts/models/progress.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ccov8lpi2n3be
|
||||
112
scripts/models/quiz_session.gd
Normal file
112
scripts/models/quiz_session.gd
Normal 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())
|
||||
1
scripts/models/quiz_session.gd.uid
Normal file
1
scripts/models/quiz_session.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b1j31h2s1bwyq
|
||||
Loading…
Reference in a new issue