new features, play menu, deck builder, deck selection
This commit is contained in:
165
scripts/data/Deck.gd
Normal file
165
scripts/data/Deck.gd
Normal file
@@ -0,0 +1,165 @@
|
||||
class_name Deck
|
||||
extends RefCounted
|
||||
|
||||
## Deck - Data model for a player's deck
|
||||
|
||||
signal deck_changed
|
||||
|
||||
const MIN_CARDS: int = 50
|
||||
const MAX_CARDS: int = 50
|
||||
const MAX_COPIES: int = 3
|
||||
|
||||
var name: String = "New Deck"
|
||||
var cards: Dictionary = {} # card_id -> count
|
||||
|
||||
|
||||
## Add a card to the deck
|
||||
## Returns empty string on success, error message on failure
|
||||
func add_card(card_id: String) -> String:
|
||||
var current_count = cards.get(card_id, 0)
|
||||
if current_count >= MAX_COPIES:
|
||||
return "Maximum %d copies allowed" % MAX_COPIES
|
||||
|
||||
var total = get_total_cards()
|
||||
if total >= MAX_CARDS:
|
||||
return "Deck is full (%d cards)" % MAX_CARDS
|
||||
|
||||
cards[card_id] = current_count + 1
|
||||
deck_changed.emit()
|
||||
return ""
|
||||
|
||||
|
||||
## Remove a card from the deck
|
||||
## Returns true if successful
|
||||
func remove_card(card_id: String) -> bool:
|
||||
if not cards.has(card_id):
|
||||
return false
|
||||
|
||||
cards[card_id] -= 1
|
||||
if cards[card_id] <= 0:
|
||||
cards.erase(card_id)
|
||||
|
||||
deck_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
## Set card count directly
|
||||
func set_card_count(card_id: String, count: int) -> void:
|
||||
if count <= 0:
|
||||
cards.erase(card_id)
|
||||
else:
|
||||
cards[card_id] = mini(count, MAX_COPIES)
|
||||
deck_changed.emit()
|
||||
|
||||
|
||||
## Get total number of cards in deck
|
||||
func get_total_cards() -> int:
|
||||
var total = 0
|
||||
for count in cards.values():
|
||||
total += count
|
||||
return total
|
||||
|
||||
|
||||
## Get count for a specific card
|
||||
func get_card_count(card_id: String) -> int:
|
||||
return cards.get(card_id, 0)
|
||||
|
||||
|
||||
## Get all unique card IDs in deck
|
||||
func get_card_ids() -> Array[String]:
|
||||
var ids: Array[String] = []
|
||||
for card_id in cards.keys():
|
||||
ids.append(card_id)
|
||||
return ids
|
||||
|
||||
|
||||
## Validate the deck
|
||||
## Returns array of error messages (empty if valid)
|
||||
func validate() -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
var total = get_total_cards()
|
||||
|
||||
if total < MIN_CARDS:
|
||||
errors.append("Deck needs %d more cards" % (MIN_CARDS - total))
|
||||
elif total > MAX_CARDS:
|
||||
errors.append("Deck has %d too many cards" % (total - MAX_CARDS))
|
||||
|
||||
for card_id in cards:
|
||||
if cards[card_id] > MAX_COPIES:
|
||||
var card_data = CardDatabase.get_card(card_id)
|
||||
var card_name = card_data.name if card_data else card_id
|
||||
errors.append("%s has too many copies (%d)" % [card_name, cards[card_id]])
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
## Check if deck is valid
|
||||
func is_valid() -> bool:
|
||||
return validate().size() == 0
|
||||
|
||||
|
||||
## Convert deck to array of card IDs (for gameplay)
|
||||
func to_card_array() -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
for card_id in cards:
|
||||
for i in range(cards[card_id]):
|
||||
result.append(card_id)
|
||||
return result
|
||||
|
||||
|
||||
## Clear the deck
|
||||
func clear() -> void:
|
||||
cards.clear()
|
||||
deck_changed.emit()
|
||||
|
||||
|
||||
## Get deck statistics
|
||||
func get_stats() -> Dictionary:
|
||||
var stats = {
|
||||
"total": get_total_cards(),
|
||||
"unique": cards.size(),
|
||||
"elements": {},
|
||||
"types": {},
|
||||
"cost_curve": {}
|
||||
}
|
||||
|
||||
for card_id in cards:
|
||||
var count = cards[card_id]
|
||||
var card_data = CardDatabase.get_card(card_id)
|
||||
if not card_data:
|
||||
continue
|
||||
|
||||
# Element breakdown
|
||||
for element in card_data.elements:
|
||||
var elem_name = Enums.element_to_string(element)
|
||||
stats.elements[elem_name] = stats.elements.get(elem_name, 0) + count
|
||||
|
||||
# Type breakdown
|
||||
var type_name = Enums.card_type_to_string(card_data.type)
|
||||
stats.types[type_name] = stats.types.get(type_name, 0) + count
|
||||
|
||||
# Cost curve
|
||||
var cost_key = str(card_data.cost)
|
||||
stats.cost_curve[cost_key] = stats.cost_curve.get(cost_key, 0) + count
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
## Serialize deck to dictionary (for saving)
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
"cards": cards.duplicate(),
|
||||
"version": "1.0"
|
||||
}
|
||||
|
||||
|
||||
## Load deck from dictionary
|
||||
func from_dict(data: Dictionary) -> void:
|
||||
name = data.get("name", "Unnamed Deck")
|
||||
cards = data.get("cards", {}).duplicate()
|
||||
deck_changed.emit()
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "[Deck: %s (%d cards)]" % [name, get_total_cards()]
|
||||
126
scripts/data/DeckManager.gd
Normal file
126
scripts/data/DeckManager.gd
Normal file
@@ -0,0 +1,126 @@
|
||||
class_name DeckManager
|
||||
extends RefCounted
|
||||
|
||||
## DeckManager - Handles deck persistence (save/load)
|
||||
|
||||
const DECKS_DIR = "user://decks/"
|
||||
|
||||
|
||||
## Save a deck to file
|
||||
## Returns true on success
|
||||
static func save_deck(deck: Deck, filename: String) -> bool:
|
||||
# Ensure directory exists
|
||||
DirAccess.make_dir_recursive_absolute(DECKS_DIR)
|
||||
|
||||
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||
var file = FileAccess.open(path, FileAccess.WRITE)
|
||||
if not file:
|
||||
push_error("DeckManager: Failed to open file for writing: " + path)
|
||||
return false
|
||||
|
||||
var data = deck.to_dict()
|
||||
file.store_string(JSON.stringify(data, "\t"))
|
||||
file.close()
|
||||
return true
|
||||
|
||||
|
||||
## Load a deck from file
|
||||
## Returns null on failure
|
||||
static func load_deck(filename: String) -> Deck:
|
||||
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||
|
||||
if not FileAccess.file_exists(path):
|
||||
push_error("DeckManager: File not found: " + path)
|
||||
return null
|
||||
|
||||
var file = FileAccess.open(path, FileAccess.READ)
|
||||
if not file:
|
||||
push_error("DeckManager: Failed to open file for reading: " + path)
|
||||
return null
|
||||
|
||||
var json_text = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(json_text)
|
||||
if error != OK:
|
||||
push_error("DeckManager: JSON parse error: " + json.get_error_message())
|
||||
return null
|
||||
|
||||
var data = json.get_data()
|
||||
if not data is Dictionary:
|
||||
push_error("DeckManager: Invalid deck data format")
|
||||
return null
|
||||
|
||||
var deck = Deck.new()
|
||||
deck.from_dict(data)
|
||||
return deck
|
||||
|
||||
|
||||
## Delete a deck file
|
||||
## Returns true on success
|
||||
static func delete_deck(filename: String) -> bool:
|
||||
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||
|
||||
if not FileAccess.file_exists(path):
|
||||
return false
|
||||
|
||||
var dir = DirAccess.open(DECKS_DIR)
|
||||
if dir:
|
||||
return dir.remove(_sanitize_filename(filename) + ".json") == OK
|
||||
return false
|
||||
|
||||
|
||||
## List all saved decks
|
||||
## Returns array of deck names (without .json extension)
|
||||
static func list_decks() -> Array[String]:
|
||||
var decks: Array[String] = []
|
||||
|
||||
# Ensure directory exists
|
||||
DirAccess.make_dir_recursive_absolute(DECKS_DIR)
|
||||
|
||||
var dir = DirAccess.open(DECKS_DIR)
|
||||
if not dir:
|
||||
return decks
|
||||
|
||||
dir.list_dir_begin()
|
||||
var filename = dir.get_next()
|
||||
while filename != "":
|
||||
if not dir.current_is_dir() and filename.ends_with(".json"):
|
||||
decks.append(filename.trim_suffix(".json"))
|
||||
filename = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
|
||||
decks.sort()
|
||||
return decks
|
||||
|
||||
|
||||
## Check if a deck exists
|
||||
static func deck_exists(filename: String) -> bool:
|
||||
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||
return FileAccess.file_exists(path)
|
||||
|
||||
|
||||
## Generate a unique deck name
|
||||
static func generate_unique_name(base_name: String = "New Deck") -> String:
|
||||
var name = base_name
|
||||
var counter = 1
|
||||
|
||||
while deck_exists(name):
|
||||
counter += 1
|
||||
name = "%s %d" % [base_name, counter]
|
||||
|
||||
return name
|
||||
|
||||
|
||||
## Sanitize filename to prevent path traversal
|
||||
static func _sanitize_filename(filename: String) -> String:
|
||||
# Remove path separators and dangerous characters
|
||||
var sanitized = filename.replace("/", "_").replace("\\", "_")
|
||||
sanitized = sanitized.replace("..", "_").replace(":", "_")
|
||||
# Trim whitespace
|
||||
sanitized = sanitized.strip_edges()
|
||||
# Ensure not empty
|
||||
if sanitized.is_empty():
|
||||
sanitized = "deck"
|
||||
return sanitized
|
||||
Reference in New Issue
Block a user