commit 94847a50a2267ea2f4cf625325d5a84047b14811 Author: mr0xb Date: Sat Dec 27 00:20:33 2025 -0500 initial commit diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..296d01b --- /dev/null +++ b/project.godot @@ -0,0 +1,31 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Flashcard Game" +run/main_scene="res://scenes/main/main.tscn" +config/features=PackedStringArray("4.5", "Forward Plus") +config/icon="res://icon.svg" + +[autoload] + +DataManager="*res://scenes/autoloads/data_manager.tscn" +GameManager="*res://scenes/autoloads/game_manager.tscn" + +[display] + +window/size/viewport_width=1280 +window/size/viewport_height=720 +window/stretch/mode="canvas_items" + +[rendering] + +textures/canvas_textures/default_texture_filter=2 diff --git a/scenes/autoloads/data_manager.gd b/scenes/autoloads/data_manager.gd new file mode 100644 index 0000000..623be34 --- /dev/null +++ b/scenes/autoloads/data_manager.gd @@ -0,0 +1,321 @@ +extends Node + +signal decks_loaded() +signal deck_saved(deck: Deck) +signal deck_deleted(deck_id: String) +signal progress_updated(deck_id: String) + +const DECKS_DIR = "user://data/decks/" +const PROGRESS_FILE = "user://data/progress/progress.json" + +var decks: Array[Deck] = [] +var progress_data: Dictionary = {} +var user_id: String = "local" + +func _ready() -> void: + _ensure_directories_exist() + load_all_decks() + load_progress() + +func _ensure_directories_exist() -> void: + DirAccess.make_dir_recursive_absolute("user://data/decks/") + DirAccess.make_dir_recursive_absolute("user://data/progress/") + +func load_all_decks() -> void: + decks.clear() + var dir = DirAccess.open(DECKS_DIR) + + if not dir: + push_warning("Could not open decks directory, creating it...") + _ensure_directories_exist() + decks_loaded.emit() + return + + dir.list_dir_begin() + var file_name = dir.get_next() + + while file_name != "": + if file_name.ends_with(".json"): + var deck = load_deck_from_file(DECKS_DIR + file_name) + if deck: + decks.append(deck) + file_name = dir.get_next() + + dir.list_dir_end() + + decks.sort_custom(func(a, b): return a.updated_at > b.updated_at) + + print("Loaded %d deck(s)" % decks.size()) + decks_loaded.emit() + +func load_deck_from_file(file_path: String) -> Deck: + print("Loading Deck From File: ", file_path) + var file = FileAccess.open(file_path, FileAccess.READ) + if not file: + push_error("Failed to open deck file: " + file_path) + return null + + var json_string = file.get_as_text() + file.close() + + var json = JSON.new() + var error = json.parse(json_string) + + if error != OK: + push_error("Failed to parse JSON in file: %s (Error: %s)" % [file_path, json.get_error_message()]) + return null + + var deck = Deck.new() + deck.from_dict(json.data) + return deck + +func save_deck(deck: Deck) -> bool: + if not deck: + push_error("Cannot save null deck") + return false + + deck.updated_at = Time.get_datetime_string_from_system(true, true) + "Z" + + var file_path = DECKS_DIR + deck.id + ".json" + print("Saving Deck to: ", file_path) + var file = FileAccess.open(file_path, FileAccess.WRITE) + + if not file: + push_error("Failed to create deck file: " + file_path) + return false + + var deck_dict = deck.to_dict() + var json_string = JSON.stringify(deck_dict, "\t") + file.store_string(json_string) + file.close() + + var found = false + for i in range(decks.size()): + if decks[i].id == deck.id: + decks[i] = deck + found = true + break + + if not found: + decks.append(deck) + + decks.sort_custom(func(a, b): return a.updated_at > b.updated_at) + + print("Saved deck: %s (%d cards)" % [deck.name, deck.get_card_count()]) + deck_saved.emit(deck) + return true + +func delete_deck(deck_id: String) -> bool: + var file_path = DECKS_DIR + deck_id + ".json" + + if FileAccess.file_exists(file_path): + var error = DirAccess.remove_absolute(file_path) + if error != OK: + push_error("Failed to delete deck file: " + file_path) + return false + + for i in range(decks.size()): + if decks[i].id == deck_id: + decks.remove_at(i) + break + + if progress_data.has(deck_id): + progress_data.erase(deck_id) + save_progress() + + print("Deleted deck: " + deck_id) + deck_deleted.emit(deck_id) + return true + +func get_deck_by_id(deck_id: String) -> Deck: + for deck in decks: + if deck.id == deck_id: + return deck + return null + +func load_progress() -> void: + if not FileAccess.file_exists(PROGRESS_FILE): + print("No progress file found, starting fresh") + return + + var file = FileAccess.open(PROGRESS_FILE, FileAccess.READ) + if not file: + push_error("Failed to open progress file") + return + + var json_string = file.get_as_text() + file.close() + + var json = JSON.new() + var error = json.parse(json_string) + + if error != OK: + push_error("Failed to parse progress JSON") + return + + var data = json.data + user_id = data.get("user_id", "local") + + var deck_progress = data.get("deck_progress", {}) + for deck_id in deck_progress.keys(): + var progress = Progress.new() + progress.from_dict(deck_progress[deck_id]) + progress_data[deck_id] = progress + + print("Loaded progress for %d deck(s)" % progress_data.size()) + +func save_progress() -> void: + var deck_progress = {} + for deck_id in progress_data.keys(): + deck_progress[deck_id] = progress_data[deck_id].to_dict() + + var data = { + "version": "1.0", + "user_id": user_id, + "deck_progress": deck_progress + } + + var file = FileAccess.open(PROGRESS_FILE, FileAccess.WRITE) + if not file: + push_error("Failed to save progress file") + return + + var json_string = JSON.stringify(data, "\t") + file.store_string(json_string) + file.close() + + print("Saved progress for %d deck(s)" % progress_data.size()) + +func get_or_create_progress(deck_id: String) -> Progress: + if not progress_data.has(deck_id): + var progress = Progress.new() + progress.deck_id = deck_id + progress_data[deck_id] = progress + + return progress_data[deck_id] + +func record_quiz_result(deck_id: String, card_id: String, is_correct: bool) -> void: + var progress = get_or_create_progress(deck_id) + progress.record_answer(card_id, is_correct) + save_progress() + progress_updated.emit(deck_id) + +func get_progress(deck_id: String) -> Progress: + return progress_data.get(deck_id, null) + + +func download_deck_from_api(api_url: String) -> void: + print("Downloading deck from API: " + api_url) + + var http_request = HTTPRequest.new() + add_child(http_request) + + http_request.request_completed.connect(_on_api_deck_downloaded.bind(http_request)) + var error = http_request.request(api_url) + + if error != OK: + push_error("Failed to start HTTP request: " + str(error)) + http_request.queue_free() + +func _on_api_deck_downloaded(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, http_request: HTTPRequest) -> void: + if not http_request: + return + + if result != HTTPRequest.RESULT_SUCCESS: + push_error("HTTP request failed with result: " + str(result)) + http_request.queue_free() + return + + if response_code != 200: + push_error("API returned error code: " + str(response_code)) + http_request.queue_free() + return + + var json_string = body.get_string_from_utf8() + var json = JSON.new() + var parse_error = json.parse(json_string) + + if parse_error != OK: + push_error("Failed to parse API response JSON") + http_request.queue_free() + return + + var deck = Deck.new() + deck.from_dict(json.data) + save_deck(deck) + + print("Successfully downloaded and saved deck: " + deck.name) + http_request.queue_free() + +func upload_deck_to_api(deck: Deck, api_url: String, auth_token: String = "") -> void: + print("Uploading deck to API: " + deck.name) + + var http_request = HTTPRequest.new() + add_child(http_request) + + http_request.request_completed.connect(_on_api_deck_uploaded.bind(http_request)) + + var headers = ["Content-Type: application/json"] + if auth_token != "": + headers.append("Authorization: Bearer " + auth_token) + + var deck_dict = deck.to_dict() + var json_string = JSON.stringify(deck_dict) + + var error = http_request.request(api_url, headers, HTTPClient.METHOD_POST, json_string) + + if error != OK: + push_error("Failed to start HTTP upload request: " + str(error)) + http_request.queue_free() + +func _on_api_deck_uploaded(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, http_request: HTTPRequest) -> void: + if not http_request: + return + + if result != HTTPRequest.RESULT_SUCCESS: + push_error("HTTP upload failed with result: " + str(result)) + http_request.queue_free() + return + + if response_code == 200 or response_code == 201: + print("Deck successfully uploaded to API!") + # Optionally parse response to get the deck ID from server + var json_string = body.get_string_from_utf8() + print("Server response: " + json_string) + else: + push_error("API returned error code: " + str(response_code)) + + http_request.queue_free() + +func browse_decks_from_api(api_url: String) -> void: + print("Browsing decks from API: " + api_url) + + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_api_decks_browsed.bind(http_request)) + + var error = http_request.request(api_url) + if error != OK: + push_error("Failed to start browse request: " + str(error)) + http_request.queue_free() + +func _on_api_decks_browsed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, http_request: HTTPRequest) -> void: + if not http_request: + return + + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: + push_error("Browse request failed") + http_request.queue_free() + return + + var json_string = body.get_string_from_utf8() + var json = JSON.new() + + if json.parse(json_string) == OK: + var decks_list = json.data + print("Found %d decks from API" % decks_list.size()) + + for deck_data in decks_list: + print(" - " + deck_data.get("deck", {}).get("name", "Unknown")) + + http_request.queue_free() diff --git a/scenes/autoloads/data_manager.gd.uid b/scenes/autoloads/data_manager.gd.uid new file mode 100644 index 0000000..5b3967a --- /dev/null +++ b/scenes/autoloads/data_manager.gd.uid @@ -0,0 +1 @@ +uid://bi7aug72kjewn diff --git a/scenes/autoloads/data_manager.tscn b/scenes/autoloads/data_manager.tscn new file mode 100644 index 0000000..54dd922 --- /dev/null +++ b/scenes/autoloads/data_manager.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://c5nyvx3hkldgq"] + +[ext_resource type="Script" path="res://scenes/autoloads/data_manager.gd" id="1_qw8yd"] + +[node name="DataManager" type="Node"] +script = ExtResource("1_qw8yd") diff --git a/scenes/autoloads/game_manager.gd b/scenes/autoloads/game_manager.gd new file mode 100644 index 0000000..16f8452 --- /dev/null +++ b/scenes/autoloads/game_manager.gd @@ -0,0 +1,62 @@ +extends Node + +signal scene_changed(scene_name: String) + +var current_scene: Node = null +var scene_container: Node = null + +const MAIN_MENU = "res://scenes/ui/main_menu/main_menu.tscn" +const DECK_MANAGER = "res://scenes/ui/deck_manager/deck_manager.tscn" +const QUIZ_SCENE = "res://scenes/ui/quiz_mode/quiz_scene.tscn" + +func _ready() -> void: + await get_tree().process_frame + + var root = get_tree().root + var main = root.get_node_or_null("Main") + + if main: + scene_container = main.get_node_or_null("CanvasLayer/SceneContainer") + if scene_container: + change_scene(MAIN_MENU) + else: + push_error("SceneContainer not found in Main scene") + else: + push_warning("Main scene not found yet, waiting for initialization") + +func change_scene(scene_path: String) -> void: + if not scene_container: + push_error("Cannot change scene - scene_container is null") + return + + if current_scene: + current_scene.queue_free() + await current_scene.tree_exited + + if not ResourceLoader.exists(scene_path): + push_error("Scene does not exist: " + scene_path) + return + + var new_scene_resource = load(scene_path) + if not new_scene_resource: + push_error("Failed to load scene: " + scene_path) + return + + var new_scene = new_scene_resource.instantiate() + scene_container.add_child(new_scene) + current_scene = new_scene + + print("Changed scene to: " + scene_path) + scene_changed.emit(scene_path) + +func start_quiz(deck: Deck) -> void: + change_scene(QUIZ_SCENE) + await scene_changed + if current_scene and current_scene.has_method("start_quiz"): + current_scene.start_quiz(deck) + +func return_to_main_menu() -> void: + change_scene(MAIN_MENU) + +func open_deck_manager() -> void: + change_scene(DECK_MANAGER) diff --git a/scenes/autoloads/game_manager.gd.uid b/scenes/autoloads/game_manager.gd.uid new file mode 100644 index 0000000..89b0aec --- /dev/null +++ b/scenes/autoloads/game_manager.gd.uid @@ -0,0 +1 @@ +uid://cpotifeq44v5p diff --git a/scenes/autoloads/game_manager.tscn b/scenes/autoloads/game_manager.tscn new file mode 100644 index 0000000..8e01429 --- /dev/null +++ b/scenes/autoloads/game_manager.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://dv8yxmk7qp5qn"] + +[ext_resource type="Script" path="res://scenes/autoloads/game_manager.gd" id="1_8xr2p"] + +[node name="GameManager" type="Node"] +script = ExtResource("1_8xr2p") diff --git a/scenes/main/main.gd b/scenes/main/main.gd new file mode 100644 index 0000000..f9df2ba --- /dev/null +++ b/scenes/main/main.gd @@ -0,0 +1,6 @@ +extends Node2D + +@onready var scene_container = $CanvasLayer/SceneContainer + +func _ready() -> void: + print("Main scene ready") diff --git a/scenes/main/main.gd.uid b/scenes/main/main.gd.uid new file mode 100644 index 0000000..695572d --- /dev/null +++ b/scenes/main/main.gd.uid @@ -0,0 +1 @@ +uid://c14ki8pseb0np diff --git a/scenes/main/main.tscn b/scenes/main/main.tscn new file mode 100644 index 0000000..fba087f --- /dev/null +++ b/scenes/main/main.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=2 format=3 uid="uid://bev87hg3nq35t"] + +[ext_resource type="Script" uid="uid://c14ki8pseb0np" path="res://scenes/main/main.gd" id="1_y5mkt"] + +[node name="Main" type="Node2D"] +script = ExtResource("1_y5mkt") + +[node name="CanvasLayer" type="CanvasLayer" parent="."] + +[node name="Background" type="ColorRect" parent="CanvasLayer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.15, 0.15, 0.2, 1) + +[node name="SceneContainer" type="Node" parent="CanvasLayer"] diff --git a/scenes/ui/deck_manager/deck_editor.gd b/scenes/ui/deck_manager/deck_editor.gd new file mode 100644 index 0000000..927b093 --- /dev/null +++ b/scenes/ui/deck_manager/deck_editor.gd @@ -0,0 +1,106 @@ +extends Panel + +signal deck_saved(deck: Deck) +signal editor_closed() + +var current_deck: Deck + +@onready var deck_name_edit = $MarginContainer/VBoxContainer/DeckInfoSection/DeckNameEdit +@onready var deck_desc_edit = $MarginContainer/VBoxContainer/DeckInfoSection/DeckDescEdit +@onready var color_picker = $MarginContainer/VBoxContainer/DeckInfoSection/ColorPicker +@onready var cards_list = $MarginContainer/VBoxContainer/CardsSection/ScrollContainer/CardsList +@onready var save_btn = $MarginContainer/VBoxContainer/ButtonsSection/SaveButton +@onready var cancel_btn = $MarginContainer/VBoxContainer/ButtonsSection/CancelButton +@onready var add_card_btn = $MarginContainer/VBoxContainer/CardsSection/AddCardButton + +func _ready() -> void: + save_btn.pressed.connect(_on_save_pressed) + cancel_btn.pressed.connect(_on_cancel_pressed) + add_card_btn.pressed.connect(_on_add_card_pressed) + hide() + +func edit_deck(deck: Deck) -> void: + current_deck = deck + + deck_name_edit.text = deck.name + deck_desc_edit.text = deck.description + color_picker.color = deck.color_theme + + _refresh_cards_list() + + show() + +func _refresh_cards_list() -> void: + for child in cards_list.get_children(): + child.queue_free() + + for i in range(current_deck.cards.size()): + var card = current_deck.cards[i] + _add_card_ui_entry(card, i) + +func _add_card_ui_entry(card: Flashcard, index: int) -> void: + var card_panel = PanelContainer.new() + var margin = MarginContainer.new() + margin.add_theme_constant_override("margin_left", 10) + margin.add_theme_constant_override("margin_top", 10) + margin.add_theme_constant_override("margin_right", 10) + margin.add_theme_constant_override("margin_bottom", 10) + card_panel.add_child(margin) + + var vbox = VBoxContainer.new() + margin.add_child(vbox) + + var q_hbox = HBoxContainer.new() + vbox.add_child(q_hbox) + var q_label = Label.new() + q_label.text = "Q: " + q_hbox.add_child(q_label) + var q_edit = LineEdit.new() + q_edit.text = card.question_text + q_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL + q_edit.text_changed.connect(func(new_text): card.question_text = new_text) + q_hbox.add_child(q_edit) + + var a_hbox = HBoxContainer.new() + vbox.add_child(a_hbox) + var a_label = Label.new() + a_label.text = "A: " + a_hbox.add_child(a_label) + var a_edit = LineEdit.new() + a_edit.text = card.answer_text + a_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL + a_edit.text_changed.connect(func(new_text): card.answer_text = new_text) + a_hbox.add_child(a_edit) + + var delete_btn = Button.new() + delete_btn.text = "Delete" + delete_btn.pressed.connect(func(): _on_delete_card_pressed(index)) + vbox.add_child(delete_btn) + + cards_list.add_child(card_panel) + +func _on_add_card_pressed() -> void: + var new_card = Flashcard.new() + new_card.question_text = "New Question" + new_card.answer_text = "New Answer" + current_deck.add_card(new_card) + _refresh_cards_list() + +func _on_delete_card_pressed(index: int) -> void: + if index >= 0 and index < current_deck.cards.size(): + current_deck.cards.remove_at(index) + _refresh_cards_list() + +func _on_save_pressed() -> void: + current_deck.name = deck_name_edit.text + current_deck.description = deck_desc_edit.text + current_deck.color_theme = color_picker.color + + DataManager.save_deck(current_deck) + + deck_saved.emit(current_deck) + hide() + +func _on_cancel_pressed() -> void: + editor_closed.emit() + hide() diff --git a/scenes/ui/deck_manager/deck_editor.gd.uid b/scenes/ui/deck_manager/deck_editor.gd.uid new file mode 100644 index 0000000..757458f --- /dev/null +++ b/scenes/ui/deck_manager/deck_editor.gd.uid @@ -0,0 +1 @@ +uid://caqx35wgjp2v0 diff --git a/scenes/ui/deck_manager/deck_editor.tscn b/scenes/ui/deck_manager/deck_editor.tscn new file mode 100644 index 0000000..3c2101b --- /dev/null +++ b/scenes/ui/deck_manager/deck_editor.tscn @@ -0,0 +1,100 @@ +[gd_scene load_steps=2 format=3 uid="uid://c4p1nfvj0oy7k"] + +[ext_resource type="Script" path="res://scenes/ui/deck_manager/deck_editor.gd" id="1_b0nwp"] + +[node name="DeckEditor" type="Panel"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_b0nwp") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 60 +theme_override_constants/margin_top = 60 +theme_override_constants/margin_right = 60 +theme_override_constants/margin_bottom = 60 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Edit Deck" +horizontal_alignment = 1 + +[node name="DeckInfoSection" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="NameLabel" type="Label" parent="MarginContainer/VBoxContainer/DeckInfoSection"] +layout_mode = 2 +text = "Deck Name:" + +[node name="DeckNameEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/DeckInfoSection"] +layout_mode = 2 +placeholder_text = "Enter deck name" + +[node name="DescLabel" type="Label" parent="MarginContainer/VBoxContainer/DeckInfoSection"] +layout_mode = 2 +text = "Description:" + +[node name="DeckDescEdit" type="TextEdit" parent="MarginContainer/VBoxContainer/DeckInfoSection"] +custom_minimum_size = Vector2(0, 60) +layout_mode = 2 +placeholder_text = "Enter deck description" + +[node name="ColorLabel" type="Label" parent="MarginContainer/VBoxContainer/DeckInfoSection"] +layout_mode = 2 +text = "Deck Color:" + +[node name="ColorPicker" type="ColorPickerButton" parent="MarginContainer/VBoxContainer/DeckInfoSection"] +layout_mode = 2 + +[node name="CardsSection" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 10 + +[node name="CardsLabel" type="Label" parent="MarginContainer/VBoxContainer/CardsSection"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Cards:" + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/CardsSection"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="CardsList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/CardsSection/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 10 + +[node name="AddCardButton" type="Button" parent="MarginContainer/VBoxContainer/CardsSection"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +text = "+ Add Card" + +[node name="ButtonsSection" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 +alignment = 1 + +[node name="CancelButton" type="Button" parent="MarginContainer/VBoxContainer/ButtonsSection"] +custom_minimum_size = Vector2(150, 50) +layout_mode = 2 +text = "Cancel" + +[node name="SaveButton" type="Button" parent="MarginContainer/VBoxContainer/ButtonsSection"] +custom_minimum_size = Vector2(150, 50) +layout_mode = 2 +text = "Save Deck" diff --git a/scenes/ui/deck_manager/deck_list_item.gd b/scenes/ui/deck_manager/deck_list_item.gd new file mode 100644 index 0000000..eb8148e --- /dev/null +++ b/scenes/ui/deck_manager/deck_list_item.gd @@ -0,0 +1,45 @@ +extends PanelContainer + +signal study_requested(deck: Deck) +signal edit_requested(deck: Deck) +signal delete_requested(deck: Deck) + +var deck: Deck + +@onready var deck_name_label = $MarginContainer/HBoxContainer/VBoxContainer/DeckName +@onready var deck_info_label = $MarginContainer/HBoxContainer/VBoxContainer/DeckInfo +@onready var color_indicator = $MarginContainer/HBoxContainer/ColorIndicator +@onready var study_btn = $MarginContainer/HBoxContainer/Actions/StudyButton +@onready var edit_btn = $MarginContainer/HBoxContainer/Actions/EditButton +@onready var delete_btn = $MarginContainer/HBoxContainer/Actions/DeleteButton + +func _ready() -> void: + study_btn.pressed.connect(_on_study_pressed) + edit_btn.pressed.connect(_on_edit_pressed) + delete_btn.pressed.connect(_on_delete_pressed) + +func set_deck(p_deck: Deck) -> void: + deck = p_deck + + if deck: + deck_name_label.text = deck.name + deck_info_label.text = "%d cards" % deck.get_card_count() + color_indicator.color = deck.color_theme + + var progress = DataManager.get_progress(deck.id) + if progress: + deck_info_label.text += " • %.1f%% accuracy" % progress.get_accuracy() + +func _on_study_pressed() -> void: + if deck and deck.get_card_count() > 0: + study_requested.emit(deck) + else: + push_warning("Cannot study empty deck") + +func _on_edit_pressed() -> void: + if deck: + edit_requested.emit(deck) + +func _on_delete_pressed() -> void: + if deck: + delete_requested.emit(deck) diff --git a/scenes/ui/deck_manager/deck_list_item.gd.uid b/scenes/ui/deck_manager/deck_list_item.gd.uid new file mode 100644 index 0000000..06a5310 --- /dev/null +++ b/scenes/ui/deck_manager/deck_list_item.gd.uid @@ -0,0 +1 @@ +uid://uu8j34y86hs3 diff --git a/scenes/ui/deck_manager/deck_list_item.tscn b/scenes/ui/deck_manager/deck_list_item.tscn new file mode 100644 index 0000000..7d2849b --- /dev/null +++ b/scenes/ui/deck_manager/deck_list_item.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=2 format=3 uid="uid://clrpqfuax7qhv"] + +[ext_resource type="Script" path="res://scenes/ui/deck_manager/deck_list_item.gd" id="1_qp4wn"] + +[node name="DeckListItem" type="PanelContainer"] +custom_minimum_size = Vector2(0, 80) +script = ExtResource("1_qp4wn") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="ColorIndicator" type="ColorRect" parent="MarginContainer/HBoxContainer"] +custom_minimum_size = Vector2(8, 0) +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 5 + +[node name="DeckName" type="Label" parent="MarginContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Deck Name" + +[node name="DeckInfo" type="Label" parent="MarginContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) +theme_override_font_sizes/font_size = 14 +text = "0 cards" + +[node name="Actions" type="HBoxContainer" parent="MarginContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="StudyButton" type="Button" parent="MarginContainer/HBoxContainer/Actions"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Study" + +[node name="EditButton" type="Button" parent="MarginContainer/HBoxContainer/Actions"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Edit" + +[node name="DeleteButton" type="Button" parent="MarginContainer/HBoxContainer/Actions"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Delete" diff --git a/scenes/ui/deck_manager/deck_manager.gd b/scenes/ui/deck_manager/deck_manager.gd new file mode 100644 index 0000000..6d5b3c6 --- /dev/null +++ b/scenes/ui/deck_manager/deck_manager.gd @@ -0,0 +1,89 @@ +extends Control + +const DeckListItemScene = preload("res://scenes/ui/deck_manager/deck_list_item.tscn") + +@onready var back_btn = $MarginContainer/VBoxContainer/Header/BackButton +@onready var browse_online_btn = $MarginContainer/VBoxContainer/Header/BrowseOnlineButton +@onready var create_deck_btn = $MarginContainer/VBoxContainer/Header/CreateDeckButton +@onready var deck_list_container = $MarginContainer/VBoxContainer/ScrollContainer/DeckListContainer +@onready var modal_overlay = $ModalOverlay +@onready var deck_editor = $DeckEditor +@onready var online_browser = $OnlineDeckBrowser + +func _ready() -> void: + back_btn.pressed.connect(_on_back_pressed) + browse_online_btn.pressed.connect(_on_browse_online_pressed) + create_deck_btn.pressed.connect(_on_create_deck_pressed) + + DataManager.decks_loaded.connect(_refresh_deck_list) + DataManager.deck_saved.connect(_on_deck_saved) + DataManager.deck_deleted.connect(_on_deck_deleted) + + deck_editor.deck_saved.connect(_on_editor_deck_saved) + deck_editor.editor_closed.connect(_on_editor_closed) + + online_browser.browser_closed.connect(_on_browser_closed) + + _refresh_deck_list() + +func _refresh_deck_list() -> void: + for child in deck_list_container.get_children(): + child.queue_free() + + for deck in DataManager.decks: + var item = DeckListItemScene.instantiate() + deck_list_container.add_child(item) + item.set_deck(deck) + + item.study_requested.connect(_on_study_deck) + item.edit_requested.connect(_on_edit_deck) + item.delete_requested.connect(_on_delete_deck) + + print("Refreshed deck list with %d deck(s)" % DataManager.decks.size()) + +func _on_back_pressed() -> void: + GameManager.return_to_main_menu() + +func _on_create_deck_pressed() -> void: + var new_deck = Deck.new() + new_deck.name = "New Deck" + new_deck.description = "A new flashcard deck" + + var sample_card = Flashcard.new() + sample_card.question_text = "Sample Question" + sample_card.answer_text = "Sample Answer" + new_deck.add_card(sample_card) + + DataManager.save_deck(new_deck) + +func _on_study_deck(deck: Deck) -> void: + GameManager.start_quiz(deck) + +func _on_edit_deck(deck: Deck) -> void: + modal_overlay.show() + deck_editor.edit_deck(deck) + +func _on_delete_deck(deck: Deck) -> void: + print("Deleting deck: " + deck.name) + DataManager.delete_deck(deck.id) + +func _on_deck_saved(deck: Deck) -> void: + _refresh_deck_list() + +func _on_deck_deleted(deck_id: String) -> void: + _refresh_deck_list() + +func _on_editor_deck_saved(deck: Deck) -> void: + _refresh_deck_list() + +func _on_editor_closed() -> void: + modal_overlay.hide() + +func _on_browse_online_pressed() -> void: + modal_overlay.show() + online_browser.open_browser() + +func _on_browser_closed() -> void: + modal_overlay.hide() + # Refresh the deck list in case new decks were downloaded + _refresh_deck_list() diff --git a/scenes/ui/deck_manager/deck_manager.gd.uid b/scenes/ui/deck_manager/deck_manager.gd.uid new file mode 100644 index 0000000..269d23e --- /dev/null +++ b/scenes/ui/deck_manager/deck_manager.gd.uid @@ -0,0 +1 @@ +uid://becqmtpte7ysr diff --git a/scenes/ui/deck_manager/deck_manager.tscn b/scenes/ui/deck_manager/deck_manager.tscn new file mode 100644 index 0000000..05e17b9 --- /dev/null +++ b/scenes/ui/deck_manager/deck_manager.tscn @@ -0,0 +1,84 @@ +[gd_scene load_steps=4 format=3 uid="uid://bpd7dt0cpoh1m"] + +[ext_resource type="Script" uid="uid://becqmtpte7ysr" path="res://scenes/ui/deck_manager/deck_manager.gd" id="1_jh7lm"] +[ext_resource type="PackedScene" path="res://scenes/ui/deck_manager/deck_editor.tscn" id="2_deck_editor"] +[ext_resource type="PackedScene" uid="uid://chj1kxack8ean" path="res://scenes/ui/deck_manager/online_deck_browser.tscn" id="3_online_browser"] + +[node name="DeckManager" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_jh7lm") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 40 +theme_override_constants/margin_top = 40 +theme_override_constants/margin_right = 40 +theme_override_constants/margin_bottom = 40 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Header" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="BackButton" type="Button" parent="MarginContainer/VBoxContainer/Header"] +custom_minimum_size = Vector2(100, 50) +layout_mode = 2 +text = "← Back" + +[node name="Title" type="Label" parent="MarginContainer/VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 32 +text = "Deck Manager" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="BrowseOnlineButton" type="Button" parent="MarginContainer/VBoxContainer/Header"] +custom_minimum_size = Vector2(150, 50) +layout_mode = 2 +text = "🌐 Browse Online" + +[node name="CreateDeckButton" type="Button" parent="MarginContainer/VBoxContainer/Header"] +custom_minimum_size = Vector2(150, 50) +layout_mode = 2 +text = "+ New Deck" + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="DeckListContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 10 + +[node name="ModalOverlay" type="ColorRect" parent="."] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.9411765) + +[node name="DeckEditor" parent="." instance=ExtResource("2_deck_editor")] +visible = false +layout_mode = 1 + +[node name="OnlineDeckBrowser" parent="." instance=ExtResource("3_online_browser")] +visible = false +layout_mode = 1 diff --git a/scenes/ui/deck_manager/online_deck_browser.gd b/scenes/ui/deck_manager/online_deck_browser.gd new file mode 100644 index 0000000..87a44b2 --- /dev/null +++ b/scenes/ui/deck_manager/online_deck_browser.gd @@ -0,0 +1,133 @@ +extends Panel + +signal browser_closed() + +const API_BASE_URL = "http://localhost:8080" + +@onready var close_btn = $MarginContainer/VBoxContainer/Header/CloseButton +@onready var refresh_btn = $MarginContainer/VBoxContainer/Header/RefreshButton +@onready var status_label = $MarginContainer/VBoxContainer/StatusLabel +@onready var decks_container = $MarginContainer/VBoxContainer/ScrollContainer/DecksContainer + +var http_request: HTTPRequest + +func _ready() -> void: + close_btn.pressed.connect(_on_close_pressed) + refresh_btn.pressed.connect(_on_refresh_pressed) + hide() + +func open_browser() -> void: + show() + _load_online_decks() + +func _load_online_decks() -> void: + status_label.text = "Loading decks from API..." + _clear_deck_list() + + if http_request: + http_request.queue_free() + + http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_decks_loaded) + + var error = http_request.request(API_BASE_URL + "/decks") + if error != OK: + status_label.text = "Error: Failed to connect to API server" + print("HTTP request error: ", error) + +func _on_decks_loaded(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS: + status_label.text = "Error: Failed to load decks" + return + + if response_code != 200: + status_label.text = "Error: API returned code " + str(response_code) + return + + var json_string = body.get_string_from_utf8() + var json = JSON.new() + var parse_error = json.parse(json_string) + + if parse_error != OK: + status_label.text = "Error: Failed to parse response" + return + + var decks_list = json.data + if decks_list.is_empty(): + status_label.text = "No decks available on server" + return + + status_label.text = "Found %d deck(s) online" % decks_list.size() + _display_decks(decks_list) + +func _display_decks(decks_list: Array) -> void: + _clear_deck_list() + + for deck_data in decks_list: + var deck_info = deck_data.get("deck", {}) + var metadata = deck_info.get("metadata", {}) + var author_info = deck_info.get("author", {}) + + var deck_item = PanelContainer.new() + var margin = MarginContainer.new() + margin.add_theme_constant_override("margin_left", 15) + margin.add_theme_constant_override("margin_top", 15) + margin.add_theme_constant_override("margin_right", 15) + margin.add_theme_constant_override("margin_bottom", 15) + deck_item.add_child(margin) + + var vbox = VBoxContainer.new() + margin.add_child(vbox) + + var name_label = Label.new() + name_label.text = deck_info.get("name", "Unknown Deck") + name_label.add_theme_font_size_override("font_size", 20) + vbox.add_child(name_label) + + var desc_label = Label.new() + desc_label.text = deck_info.get("description", "") + desc_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) + vbox.add_child(desc_label) + + var cards_count = deck_info.get("cards", []).size() + var rating = metadata.get("rating", 0.0) + var downloads = metadata.get("download_count", 0) + var author_name = author_info.get("name", "N/A") + + var meta_label = Label.new() + meta_label.text = "%d cards • ⭐ %.1f • %d downloads 🧑 %s" % [cards_count, rating, downloads, author_name] + meta_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) + meta_label.add_theme_font_size_override("font_size", 14) + vbox.add_child(meta_label) + + var download_btn = Button.new() + download_btn.text = "Download Deck" + download_btn.custom_minimum_size = Vector2(150, 40) + download_btn.pressed.connect(_on_download_deck.bind(deck_info.get("id", ""))) + vbox.add_child(download_btn) + + decks_container.add_child(deck_item) + +func _on_download_deck(deck_id: String) -> void: + if deck_id == "": + return + + status_label.text = "Downloading deck..." + print("Downloading deck: " + deck_id) + + DataManager.download_deck_from_api(API_BASE_URL + "/decks/" + deck_id) + + await get_tree().create_timer(1.0).timeout + status_label.text = "Deck downloaded! Check your deck list." + +func _clear_deck_list() -> void: + for child in decks_container.get_children(): + child.queue_free() + +func _on_refresh_pressed() -> void: + _load_online_decks() + +func _on_close_pressed() -> void: + browser_closed.emit() + hide() diff --git a/scenes/ui/deck_manager/online_deck_browser.gd.uid b/scenes/ui/deck_manager/online_deck_browser.gd.uid new file mode 100644 index 0000000..2f5cd1f --- /dev/null +++ b/scenes/ui/deck_manager/online_deck_browser.gd.uid @@ -0,0 +1 @@ +uid://b1tcvxtd8urjl diff --git a/scenes/ui/deck_manager/online_deck_browser.tscn b/scenes/ui/deck_manager/online_deck_browser.tscn new file mode 100644 index 0000000..55d87b6 --- /dev/null +++ b/scenes/ui/deck_manager/online_deck_browser.tscn @@ -0,0 +1,62 @@ +[gd_scene load_steps=2 format=3 uid="uid://chj1kxack8ean"] + +[ext_resource type="Script" uid="uid://b1tcvxtd8urjl" path="res://scenes/ui/deck_manager/online_deck_browser.gd" id="1_online"] + +[node name="OnlineDeckBrowser" type="Panel"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_online") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 60 +theme_override_constants/margin_top = 60 +theme_override_constants/margin_right = 60 +theme_override_constants/margin_bottom = 60 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Header" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Title" type="Label" parent="MarginContainer/VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 32 +text = "Browse Online Decks" +horizontal_alignment = 1 + +[node name="RefreshButton" type="Button" parent="MarginContainer/VBoxContainer/Header"] +custom_minimum_size = Vector2(120, 50) +layout_mode = 2 +text = "🔄 Refresh" + +[node name="CloseButton" type="Button" parent="MarginContainer/VBoxContainer/Header"] +custom_minimum_size = Vector2(100, 50) +layout_mode = 2 +text = "Close" + +[node name="StatusLabel" type="Label" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 16 +text = "Loading..." +horizontal_alignment = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="DecksContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 15 diff --git a/scenes/ui/main_menu/main_menu.gd b/scenes/ui/main_menu/main_menu.gd new file mode 100644 index 0000000..53c5c2d --- /dev/null +++ b/scenes/ui/main_menu/main_menu.gd @@ -0,0 +1,19 @@ +extends Control + +@onready var manage_decks_btn = $MarginContainer/VBoxContainer/MenuButtons/ManageDecksButton +@onready var view_progress_btn = $MarginContainer/VBoxContainer/MenuButtons/ViewProgressButton +@onready var quit_btn = $MarginContainer/VBoxContainer/MenuButtons/QuitButton + +func _ready() -> void: + manage_decks_btn.pressed.connect(_on_manage_decks_pressed) + view_progress_btn.pressed.connect(_on_view_progress_pressed) + quit_btn.pressed.connect(_on_quit_pressed) + +func _on_manage_decks_pressed() -> void: + GameManager.open_deck_manager() + +func _on_view_progress_pressed() -> void: + print("View progress - not implemented yet") + +func _on_quit_pressed() -> void: + get_tree().quit() diff --git a/scenes/ui/main_menu/main_menu.gd.uid b/scenes/ui/main_menu/main_menu.gd.uid new file mode 100644 index 0000000..acf6875 --- /dev/null +++ b/scenes/ui/main_menu/main_menu.gd.uid @@ -0,0 +1 @@ +uid://u6qhaped7c3i diff --git a/scenes/ui/main_menu/main_menu.tscn b/scenes/ui/main_menu/main_menu.tscn new file mode 100644 index 0000000..71cdff6 --- /dev/null +++ b/scenes/ui/main_menu/main_menu.tscn @@ -0,0 +1,60 @@ +[gd_scene load_steps=2 format=3 uid="uid://c0ka24qxbge17"] + +[ext_resource type="Script" uid="uid://u6qhaped7c3i" path="res://scenes/ui/main_menu/main_menu.gd" id="1_pxqwl"] + +[node name="MainMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_pxqwl") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 40 +theme_override_constants/margin_top = 40 +theme_override_constants/margin_right = 40 +theme_override_constants/margin_bottom = 40 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 48 +text = "Flashcard Thing" +horizontal_alignment = 1 + +[node name="Spacer" type="Control" parent="MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 60) +layout_mode = 2 + +[node name="MenuButtons" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_constants/separation = 15 + +[node name="ManageDecksButton" type="Button" parent="MarginContainer/VBoxContainer/MenuButtons"] +custom_minimum_size = Vector2(300, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +text = "Manage Decks" + +[node name="ViewProgressButton" type="Button" parent="MarginContainer/VBoxContainer/MenuButtons"] +custom_minimum_size = Vector2(300, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +text = "View Progress" + +[node name="QuitButton" type="Button" parent="MarginContainer/VBoxContainer/MenuButtons"] +custom_minimum_size = Vector2(300, 60) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +text = "Quit" diff --git a/scenes/ui/quiz_mode/flashcard_display.gd b/scenes/ui/quiz_mode/flashcard_display.gd new file mode 100644 index 0000000..2856a19 --- /dev/null +++ b/scenes/ui/quiz_mode/flashcard_display.gd @@ -0,0 +1,48 @@ +extends PanelContainer + +signal answer_submitted(is_correct: bool) + +@onready var question_label = $MarginContainer/VBoxContainer/QuestionPanel/MarginContainer/QuestionLabel +@onready var answer_panel = $MarginContainer/VBoxContainer/AnswerPanel +@onready var answer_label = $MarginContainer/VBoxContainer/AnswerPanel/MarginContainer/AnswerLabel +@onready var show_answer_btn = $MarginContainer/VBoxContainer/Controls/ShowAnswerButton +@onready var rating_buttons = $MarginContainer/VBoxContainer/Controls/RatingButtons +@onready var incorrect_btn = $MarginContainer/VBoxContainer/Controls/RatingButtons/IncorrectButton +@onready var correct_btn = $MarginContainer/VBoxContainer/Controls/RatingButtons/CorrectButton + +var current_card: Flashcard +var answer_revealed: bool = false + +func _ready() -> void: + show_answer_btn.pressed.connect(_on_show_answer_pressed) + incorrect_btn.pressed.connect(_on_incorrect_pressed) + correct_btn.pressed.connect(_on_correct_pressed) + + answer_panel.hide() + rating_buttons.hide() + +func set_card(card: Flashcard) -> void: + current_card = card + answer_revealed = false + + question_label.text = card.question_text + question_label.modulate = card.question_color + + answer_label.text = card.answer_text + answer_label.modulate = card.answer_color + + answer_panel.hide() + rating_buttons.hide() + show_answer_btn.show() + +func _on_show_answer_pressed() -> void: + answer_revealed = true + answer_panel.show() + show_answer_btn.hide() + rating_buttons.show() + +func _on_incorrect_pressed() -> void: + answer_submitted.emit(false) + +func _on_correct_pressed() -> void: + answer_submitted.emit(true) diff --git a/scenes/ui/quiz_mode/flashcard_display.gd.uid b/scenes/ui/quiz_mode/flashcard_display.gd.uid new file mode 100644 index 0000000..6fe6b97 --- /dev/null +++ b/scenes/ui/quiz_mode/flashcard_display.gd.uid @@ -0,0 +1 @@ +uid://lobty8kshx4c diff --git a/scenes/ui/quiz_mode/flashcard_display.tscn b/scenes/ui/quiz_mode/flashcard_display.tscn new file mode 100644 index 0000000..8af277f --- /dev/null +++ b/scenes/ui/quiz_mode/flashcard_display.tscn @@ -0,0 +1,83 @@ +[gd_scene load_steps=2 format=3 uid="uid://b4nnkfplcdxkp"] + +[ext_resource type="Script" path="res://scenes/ui/quiz_mode/flashcard_display.gd" id="1_8xp2n"] + +[node name="FlashcardDisplay" type="PanelContainer"] +custom_minimum_size = Vector2(600, 400) +script = ExtResource("1_8xp2n") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 30 +theme_override_constants/margin_top = 30 +theme_override_constants/margin_right = 30 +theme_override_constants/margin_bottom = 30 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="QuestionPanel" type="PanelContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/QuestionPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="QuestionLabel" type="Label" parent="MarginContainer/VBoxContainer/QuestionPanel/MarginContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +text = "Question goes here" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="AnswerPanel" type="PanelContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/AnswerPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="AnswerLabel" type="Label" parent="MarginContainer/VBoxContainer/AnswerPanel/MarginContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +text = "Answer goes here" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="Controls" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="ShowAnswerButton" type="Button" parent="MarginContainer/VBoxContainer/Controls"] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Show Answer" + +[node name="RatingButtons" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Controls"] +layout_mode = 2 +theme_override_constants/separation = 20 +alignment = 1 + +[node name="IncorrectButton" type="Button" parent="MarginContainer/VBoxContainer/Controls/RatingButtons"] +custom_minimum_size = Vector2(200, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "❌ Incorrect" + +[node name="CorrectButton" type="Button" parent="MarginContainer/VBoxContainer/Controls/RatingButtons"] +custom_minimum_size = Vector2(200, 50) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "✓ Correct" diff --git a/scenes/ui/quiz_mode/multiple_choice_display.gd b/scenes/ui/quiz_mode/multiple_choice_display.gd new file mode 100644 index 0000000..f7b3907 --- /dev/null +++ b/scenes/ui/quiz_mode/multiple_choice_display.gd @@ -0,0 +1,67 @@ +extends PanelContainer + +signal answer_submitted(is_correct: bool) + +@onready var question_label = $MarginContainer/VBoxContainer/QuestionPanel/MarginContainer/QuestionLabel +@onready var choices_container = $MarginContainer/VBoxContainer/ChoicesContainer +@onready var feedback_panel = $MarginContainer/VBoxContainer/FeedbackPanel +@onready var feedback_label = $MarginContainer/VBoxContainer/FeedbackPanel/MarginContainer/FeedbackLabel + +var current_card: Flashcard +var choice_buttons: Array[Button] = [] +var answered: bool = false + +func _ready() -> void: + feedback_panel.hide() + +func set_card(card: Flashcard) -> void: + current_card = card + answered = false + + question_label.text = card.question_text + question_label.modulate = card.question_color + + for button in choice_buttons: + button.queue_free() + choice_buttons.clear() + + for i in range(card.choices.size()): + var choice = card.choices[i] + var button = Button.new() + button.text = choice.text + button.custom_minimum_size = Vector2(0, 50) + button.add_theme_font_size_override("font_size", 18) + button.pressed.connect(_on_choice_selected.bind(i)) + choices_container.add_child(button) + choice_buttons.append(button) + + feedback_panel.hide() + +func _on_choice_selected(choice_index: int) -> void: + if answered: + return + + answered = true + var is_correct = current_card.choices[choice_index].is_correct + + for button in choice_buttons: + button.disabled = true + + if is_correct: + choice_buttons[choice_index].modulate = Color.GREEN + feedback_label.text = "✓ Correct! " + current_card.answer_text + feedback_panel.self_modulate = Color(0.2, 0.6, 0.2, 1.0) + else: + choice_buttons[choice_index].modulate = Color.RED + + var correct_index = current_card.get_correct_choice_index() + if correct_index >= 0: + choice_buttons[correct_index].modulate = Color.GREEN + + feedback_label.text = "❌ Incorrect. The answer is: " + current_card.answer_text + feedback_panel.self_modulate = Color(0.6, 0.2, 0.2, 1.0) + + feedback_panel.show() + + await get_tree().create_timer(2.0).timeout + answer_submitted.emit(is_correct) diff --git a/scenes/ui/quiz_mode/multiple_choice_display.gd.uid b/scenes/ui/quiz_mode/multiple_choice_display.gd.uid new file mode 100644 index 0000000..667dc28 --- /dev/null +++ b/scenes/ui/quiz_mode/multiple_choice_display.gd.uid @@ -0,0 +1 @@ +uid://2cccr5qtjk2c diff --git a/scenes/ui/quiz_mode/multiple_choice_display.tscn b/scenes/ui/quiz_mode/multiple_choice_display.tscn new file mode 100644 index 0000000..8045bae --- /dev/null +++ b/scenes/ui/quiz_mode/multiple_choice_display.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=2 format=3 uid="uid://b0qexkdgxksek"] + +[ext_resource type="Script" path="res://scenes/ui/quiz_mode/multiple_choice_display.gd" id="1_a2ghr"] + +[node name="MultipleChoiceDisplay" type="PanelContainer"] +custom_minimum_size = Vector2(600, 400) +script = ExtResource("1_a2ghr") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 30 +theme_override_constants/margin_top = 30 +theme_override_constants/margin_right = 30 +theme_override_constants/margin_bottom = 30 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="QuestionPanel" type="PanelContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/QuestionPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="QuestionLabel" type="Label" parent="MarginContainer/VBoxContainer/QuestionPanel/MarginContainer"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +theme_override_font_sizes/font_size = 24 +text = "Question goes here" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="ChoicesContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="FeedbackPanel" type="PanelContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/FeedbackPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 15 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 15 + +[node name="FeedbackLabel" type="Label" parent="MarginContainer/VBoxContainer/FeedbackPanel/MarginContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Feedback" +horizontal_alignment = 1 +autowrap_mode = 3 diff --git a/scenes/ui/quiz_mode/quiz_scene.gd b/scenes/ui/quiz_mode/quiz_scene.gd new file mode 100644 index 0000000..1a4552c --- /dev/null +++ b/scenes/ui/quiz_mode/quiz_scene.gd @@ -0,0 +1,95 @@ +extends Control + +@onready var exit_btn = $MarginContainer/VBoxContainer/Header/ExitButton +@onready var progress_label = $MarginContainer/VBoxContainer/Header/ProgressLabel +@onready var score_label = $MarginContainer/VBoxContainer/ScorePanel/ScoreLabel +@onready var card_container = $MarginContainer/VBoxContainer/CardContainer + +var quiz_session: QuizSession +var current_card_display: Node + +const FlashcardDisplayScene = preload("res://scenes/ui/quiz_mode/flashcard_display.tscn") +const MultipleChoiceDisplayScene = preload("res://scenes/ui/quiz_mode/multiple_choice_display.tscn") + +func _ready() -> void: + exit_btn.pressed.connect(_on_exit_pressed) + +func start_quiz(deck: Deck) -> void: + if not deck or deck.cards.is_empty(): + push_error("Cannot start quiz with empty or null deck") + return + + quiz_session = QuizSession.new() + quiz_session.question_answered.connect(_on_question_answered) + quiz_session.session_completed.connect(_on_session_completed) + quiz_session.progress_updated.connect(_on_progress_updated) + quiz_session.start_session(deck, true) + + var progress = DataManager.get_or_create_progress(deck.id) + progress.start_session() + DataManager.save_progress() + + _show_next_card() + _update_ui() + +func _show_next_card() -> void: + var card = quiz_session.get_current_card() + + if not card: + return + + if current_card_display: + current_card_display.queue_free() + current_card_display = null + + if card.card_type == Flashcard.QuestionType.BASIC: + current_card_display = FlashcardDisplayScene.instantiate() + else: + current_card_display = MultipleChoiceDisplayScene.instantiate() + + card_container.add_child(current_card_display) + current_card_display.set_card(card) + current_card_display.answer_submitted.connect(_on_answer_submitted) + +func _on_answer_submitted(is_correct: bool) -> void: + var card = quiz_session.get_current_card() + + if quiz_session.deck: + DataManager.record_quiz_result(quiz_session.deck.id, card.id, is_correct) + + quiz_session.answer_question(is_correct) + +func _on_question_answered(is_correct: bool, card: Flashcard) -> void: + await get_tree().create_timer(0.5).timeout + _update_ui() + _show_next_card() + +func _on_progress_updated(current: int, total: int) -> void: + _update_ui() + +func _on_session_completed(results: Dictionary) -> void: + print("Quiz completed! Results:") + print(" Total: %d" % results.total_questions) + print(" Correct: %d" % results.correct) + print(" Incorrect: %d" % results.incorrect) + print(" Accuracy: %.1f%%" % results.accuracy) + print(" Duration: %.1f seconds" % results.duration_seconds) + + await get_tree().create_timer(1.0).timeout + GameManager.return_to_main_menu() + +func _update_ui() -> void: + if quiz_session: + progress_label.text = "Card %d / %d" % [ + quiz_session.current_index + 1, + quiz_session.shuffled_cards.size() + ] + + score_label.text = "Score: %d correct, %d incorrect (%.1f%%)" % [ + quiz_session.session_correct, + quiz_session.session_incorrect, + quiz_session.get_session_accuracy() + ] + +func _on_exit_pressed() -> void: + GameManager.return_to_main_menu() diff --git a/scenes/ui/quiz_mode/quiz_scene.gd.uid b/scenes/ui/quiz_mode/quiz_scene.gd.uid new file mode 100644 index 0000000..1e991a4 --- /dev/null +++ b/scenes/ui/quiz_mode/quiz_scene.gd.uid @@ -0,0 +1 @@ +uid://sfunbafcelku diff --git a/scenes/ui/quiz_mode/quiz_scene.tscn b/scenes/ui/quiz_mode/quiz_scene.tscn new file mode 100644 index 0000000..97e4a22 --- /dev/null +++ b/scenes/ui/quiz_mode/quiz_scene.tscn @@ -0,0 +1,57 @@ +[gd_scene load_steps=2 format=3 uid="uid://bjldqy3e3m3yj"] + +[ext_resource type="Script" path="res://scenes/ui/quiz_mode/quiz_scene.gd" id="1_vu2ha"] + +[node name="QuizScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_vu2ha") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 40 +theme_override_constants/margin_top = 40 +theme_override_constants/margin_right = 40 +theme_override_constants/margin_bottom = 40 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Header" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="ExitButton" type="Button" parent="MarginContainer/VBoxContainer/Header"] +custom_minimum_size = Vector2(100, 50) +layout_mode = 2 +text = "Exit Quiz" + +[node name="ProgressLabel" type="Label" parent="MarginContainer/VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 24 +text = "Card 1 / 10" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="ScorePanel" type="PanelContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="ScoreLabel" type="Label" parent="MarginContainer/VBoxContainer/ScorePanel"] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Score: 0 correct, 0 incorrect (0.0%)" +horizontal_alignment = 1 + +[node name="CardContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 diff --git a/scripts/models/deck.gd b/scripts/models/deck.gd new file mode 100644 index 0000000..41b478b --- /dev/null +++ b/scripts/models/deck.gd @@ -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() + ] diff --git a/scripts/models/deck.gd.uid b/scripts/models/deck.gd.uid new file mode 100644 index 0000000..4e013e8 --- /dev/null +++ b/scripts/models/deck.gd.uid @@ -0,0 +1 @@ +uid://dio0iqdf45ig1 diff --git a/scripts/models/flashcard.gd b/scripts/models/flashcard.gd new file mode 100644 index 0000000..2ed8e5e --- /dev/null +++ b/scripts/models/flashcard.gd @@ -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() + ] diff --git a/scripts/models/flashcard.gd.uid b/scripts/models/flashcard.gd.uid new file mode 100644 index 0000000..137d5b4 --- /dev/null +++ b/scripts/models/flashcard.gd.uid @@ -0,0 +1 @@ +uid://bhpp4v2b5cqxq diff --git a/scripts/models/progress.gd b/scripts/models/progress.gd new file mode 100644 index 0000000..259c7eb --- /dev/null +++ b/scripts/models/progress.gd @@ -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 diff --git a/scripts/models/progress.gd.uid b/scripts/models/progress.gd.uid new file mode 100644 index 0000000..2292519 --- /dev/null +++ b/scripts/models/progress.gd.uid @@ -0,0 +1 @@ +uid://ccov8lpi2n3be diff --git a/scripts/models/quiz_session.gd b/scripts/models/quiz_session.gd new file mode 100644 index 0000000..72cc2ab --- /dev/null +++ b/scripts/models/quiz_session.gd @@ -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()) diff --git a/scripts/models/quiz_session.gd.uid b/scripts/models/quiz_session.gd.uid new file mode 100644 index 0000000..76f7d89 --- /dev/null +++ b/scripts/models/quiz_session.gd.uid @@ -0,0 +1 @@ +uid://b1j31h2s1bwyq