mirror of
https://github.com/mr0xb/godot-flashcards.git
synced 2026-08-28 04:54:57 -04:00
initial commit
This commit is contained in:
commit
94847a50a2
42 changed files with 2063 additions and 0 deletions
31
project.godot
Normal file
31
project.godot
Normal file
|
|
@ -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
|
||||
321
scenes/autoloads/data_manager.gd
Normal file
321
scenes/autoloads/data_manager.gd
Normal file
|
|
@ -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()
|
||||
1
scenes/autoloads/data_manager.gd.uid
Normal file
1
scenes/autoloads/data_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bi7aug72kjewn
|
||||
6
scenes/autoloads/data_manager.tscn
Normal file
6
scenes/autoloads/data_manager.tscn
Normal file
|
|
@ -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")
|
||||
62
scenes/autoloads/game_manager.gd
Normal file
62
scenes/autoloads/game_manager.gd
Normal file
|
|
@ -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)
|
||||
1
scenes/autoloads/game_manager.gd.uid
Normal file
1
scenes/autoloads/game_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cpotifeq44v5p
|
||||
6
scenes/autoloads/game_manager.tscn
Normal file
6
scenes/autoloads/game_manager.tscn
Normal file
|
|
@ -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")
|
||||
6
scenes/main/main.gd
Normal file
6
scenes/main/main.gd
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
extends Node2D
|
||||
|
||||
@onready var scene_container = $CanvasLayer/SceneContainer
|
||||
|
||||
func _ready() -> void:
|
||||
print("Main scene ready")
|
||||
1
scenes/main/main.gd.uid
Normal file
1
scenes/main/main.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c14ki8pseb0np
|
||||
18
scenes/main/main.tscn
Normal file
18
scenes/main/main.tscn
Normal file
|
|
@ -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"]
|
||||
106
scenes/ui/deck_manager/deck_editor.gd
Normal file
106
scenes/ui/deck_manager/deck_editor.gd
Normal file
|
|
@ -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()
|
||||
1
scenes/ui/deck_manager/deck_editor.gd.uid
Normal file
1
scenes/ui/deck_manager/deck_editor.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://caqx35wgjp2v0
|
||||
100
scenes/ui/deck_manager/deck_editor.tscn
Normal file
100
scenes/ui/deck_manager/deck_editor.tscn
Normal file
|
|
@ -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"
|
||||
45
scenes/ui/deck_manager/deck_list_item.gd
Normal file
45
scenes/ui/deck_manager/deck_list_item.gd
Normal file
|
|
@ -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)
|
||||
1
scenes/ui/deck_manager/deck_list_item.gd.uid
Normal file
1
scenes/ui/deck_manager/deck_list_item.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://uu8j34y86hs3
|
||||
56
scenes/ui/deck_manager/deck_list_item.tscn
Normal file
56
scenes/ui/deck_manager/deck_list_item.tscn
Normal file
|
|
@ -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"
|
||||
89
scenes/ui/deck_manager/deck_manager.gd
Normal file
89
scenes/ui/deck_manager/deck_manager.gd
Normal file
|
|
@ -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()
|
||||
1
scenes/ui/deck_manager/deck_manager.gd.uid
Normal file
1
scenes/ui/deck_manager/deck_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://becqmtpte7ysr
|
||||
84
scenes/ui/deck_manager/deck_manager.tscn
Normal file
84
scenes/ui/deck_manager/deck_manager.tscn
Normal file
|
|
@ -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
|
||||
133
scenes/ui/deck_manager/online_deck_browser.gd
Normal file
133
scenes/ui/deck_manager/online_deck_browser.gd
Normal file
|
|
@ -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()
|
||||
1
scenes/ui/deck_manager/online_deck_browser.gd.uid
Normal file
1
scenes/ui/deck_manager/online_deck_browser.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b1tcvxtd8urjl
|
||||
62
scenes/ui/deck_manager/online_deck_browser.tscn
Normal file
62
scenes/ui/deck_manager/online_deck_browser.tscn
Normal file
|
|
@ -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
|
||||
19
scenes/ui/main_menu/main_menu.gd
Normal file
19
scenes/ui/main_menu/main_menu.gd
Normal file
|
|
@ -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()
|
||||
1
scenes/ui/main_menu/main_menu.gd.uid
Normal file
1
scenes/ui/main_menu/main_menu.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://u6qhaped7c3i
|
||||
60
scenes/ui/main_menu/main_menu.tscn
Normal file
60
scenes/ui/main_menu/main_menu.tscn
Normal file
|
|
@ -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"
|
||||
48
scenes/ui/quiz_mode/flashcard_display.gd
Normal file
48
scenes/ui/quiz_mode/flashcard_display.gd
Normal file
|
|
@ -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)
|
||||
1
scenes/ui/quiz_mode/flashcard_display.gd.uid
Normal file
1
scenes/ui/quiz_mode/flashcard_display.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://lobty8kshx4c
|
||||
83
scenes/ui/quiz_mode/flashcard_display.tscn
Normal file
83
scenes/ui/quiz_mode/flashcard_display.tscn
Normal file
|
|
@ -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"
|
||||
67
scenes/ui/quiz_mode/multiple_choice_display.gd
Normal file
67
scenes/ui/quiz_mode/multiple_choice_display.gd
Normal file
|
|
@ -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)
|
||||
1
scenes/ui/quiz_mode/multiple_choice_display.gd.uid
Normal file
1
scenes/ui/quiz_mode/multiple_choice_display.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://2cccr5qtjk2c
|
||||
58
scenes/ui/quiz_mode/multiple_choice_display.tscn
Normal file
58
scenes/ui/quiz_mode/multiple_choice_display.tscn
Normal file
|
|
@ -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
|
||||
95
scenes/ui/quiz_mode/quiz_scene.gd
Normal file
95
scenes/ui/quiz_mode/quiz_scene.gd
Normal file
|
|
@ -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()
|
||||
1
scenes/ui/quiz_mode/quiz_scene.gd.uid
Normal file
1
scenes/ui/quiz_mode/quiz_scene.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://sfunbafcelku
|
||||
57
scenes/ui/quiz_mode/quiz_scene.tscn
Normal file
57
scenes/ui/quiz_mode/quiz_scene.tscn
Normal file
|
|
@ -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
|
||||
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