extends Node ## CardDatabase - Singleton for managing card data ## Loads card definitions from JSON and provides lookup methods const CARDS_PATH = "res://data/cards.json" const STARTER_DECKS_PATH = "res://data/starter_decks.json" # Loaded card data var _cards: Dictionary = {} # id -> CardData var _cards_by_element: Dictionary = {} # Element -> Array[CardData] var _cards_by_type: Dictionary = {} # CardType -> Array[CardData] var _card_textures: Dictionary = {} # id -> Texture2D var _starter_decks: Array = [] # Array of StarterDeckData # Signals signal database_loaded signal load_error(message: String) func _ready() -> void: _load_database() _load_starter_decks() func _load_database() -> void: var file = FileAccess.open(CARDS_PATH, FileAccess.READ) if not file: push_error("Failed to open cards database: " + CARDS_PATH) load_error.emit("Failed to open cards database") return var json_text = file.get_as_text() file.close() var json = JSON.new() var error = json.parse(json_text) if error != OK: push_error("Failed to parse cards JSON: " + json.get_error_message()) load_error.emit("Failed to parse cards JSON") return var data = json.get_data() if not data.has("cards"): push_error("Cards database missing 'cards' array") load_error.emit("Invalid database format") return # Initialize element and type dictionaries for element in Enums.Element.values(): _cards_by_element[element] = [] for card_type in Enums.CardType.values(): _cards_by_type[card_type] = [] # Parse cards for card_data in data["cards"]: var card = _parse_card(card_data) if card: _cards[card.id] = card # Index by element(s) for element in card.elements: _cards_by_element[element].append(card) # Index by type _cards_by_type[card.type].append(card) print("CardDatabase: Loaded ", _cards.size(), " cards") database_loaded.emit() func _parse_card(data: Dictionary) -> CardData: var card = CardData.new() # Required fields if not data.has("id") or not data.has("name") or not data.has("type") or not data.has("element") or not data.has("cost"): push_error("Card missing required fields: " + str(data)) return null card.id = data["id"] card.name = data["name"] card.type = Enums.card_type_from_string(data["type"]) card.cost = data["cost"] # Parse element (can be string or array) if data["element"] is Array: for elem_str in data["element"]: card.elements.append(Enums.element_from_string(elem_str)) else: card.elements.append(Enums.element_from_string(data["element"])) # Optional fields card.power = data.get("power", 0) if data.get("power") != null else 0 card.job = data.get("job", "") if data.get("job") != null else "" card.category = data.get("category", "") if data.get("category") != null else "" card.is_generic = data.get("is_generic", false) if data.get("is_generic") != null else false card.has_ex_burst = data.get("has_ex_burst", false) if data.get("has_ex_burst") != null else false card.has_haste = data.get("has_haste", false) if data.get("has_haste") != null else false card.image_path = data.get("image", "") if data.get("image") != null else "" # Parse abilities if data.has("abilities"): for ability_data in data["abilities"]: var ability = _parse_ability(ability_data) if ability: card.abilities.append(ability) return card func _parse_ability(data: Dictionary) -> AbilityData: var ability = AbilityData.new() if not data.has("type") or not data.has("effect"): push_error("Ability missing required fields") return null match data["type"].to_lower(): "field": ability.type = Enums.AbilityType.FIELD "auto": ability.type = Enums.AbilityType.AUTO "action": ability.type = Enums.AbilityType.ACTION "special": ability.type = Enums.AbilityType.SPECIAL ability.effect = data["effect"] ability.name = data.get("name", "") ability.trigger = data.get("trigger", "") ability.is_ex_burst = data.get("is_ex_burst", false) # Parse cost if present if data.has("cost"): ability.cost = _parse_cost(data["cost"]) return ability func _parse_cost(data: Dictionary) -> CostData: var cost = CostData.new() cost.generic = data.get("generic", 0) cost.fire = data.get("fire", 0) cost.ice = data.get("ice", 0) cost.wind = data.get("wind", 0) cost.lightning = data.get("lightning", 0) cost.water = data.get("water", 0) cost.earth = data.get("earth", 0) cost.light = data.get("light", 0) cost.dark = data.get("dark", 0) cost.requires_dull = data.get("dull", false) cost.discard_count = data.get("discard", 0) cost.specific_discard = data.get("specific_discard", "") return cost ## Get a card by ID func get_card(id: String) -> CardData: return _cards.get(id) ## Get all cards func get_all_cards() -> Array: return _cards.values() ## Get cards by element func get_cards_by_element(element: Enums.Element) -> Array: return _cards_by_element.get(element, []) ## Get cards by type func get_cards_by_type(card_type: Enums.CardType) -> Array: return _cards_by_type.get(card_type, []) ## Filter cards by multiple criteria ## filters: Dictionary with optional keys: ## - name: String (substring search, case-insensitive) ## - elements: Array[Enums.Element] (OR logic - card has any of these) ## - type: Enums.CardType (-1 or omit for any) ## - cost_min: int ## - cost_max: int ## - job: String (exact match, case-insensitive) ## - category: String (exact match) ## - power_min: int ## - power_max: int ## - ex_burst_only: bool ## - set: String (card ID prefix, e.g. "1-", "2-") func filter_cards(filters: Dictionary) -> Array[CardData]: var results: Array[CardData] = [] for card in _cards.values(): if _matches_filters(card, filters): results.append(card) return results func _matches_filters(card: CardData, filters: Dictionary) -> bool: # Name search (case-insensitive substring) if filters.has("name") and not filters.name.is_empty(): if not card.name.to_lower().contains(filters.name.to_lower()): return false # Element filter (OR logic for multi-select) if filters.has("elements") and filters.elements.size() > 0: var has_element = false for elem in card.elements: if elem in filters.elements: has_element = true break if not has_element: return false # Type filter if filters.has("type") and filters.type != -1: if card.type != filters.type: return false # Cost filter (range) if filters.has("cost_min") and card.cost < filters.cost_min: return false if filters.has("cost_max") and card.cost > filters.cost_max: return false # Job filter (case-insensitive) if filters.has("job") and not filters.job.is_empty(): if card.job.to_lower() != filters.job.to_lower(): return false # Category filter if filters.has("category") and not filters.category.is_empty(): if card.category != filters.category: return false # Power range if filters.has("power_min") and card.power < filters.power_min: return false if filters.has("power_max") and card.power > filters.power_max: return false # EX Burst filter if filters.has("ex_burst_only") and filters.ex_burst_only: if not card.has_ex_burst: return false # Set/Opus filter (card ID prefix) if filters.has("set") and not filters.set.is_empty(): if not card.id.begins_with(filters.set): return false return true ## Get all unique job values from loaded cards func get_unique_jobs() -> Array[String]: var jobs: Dictionary = {} for card in _cards.values(): if not card.job.is_empty(): jobs[card.job] = true var result: Array[String] = [] for job in jobs.keys(): result.append(job) result.sort() return result ## Get all unique category values from loaded cards func get_unique_categories() -> Array[String]: var categories: Dictionary = {} for card in _cards.values(): if not card.category.is_empty(): categories[card.category] = true var result: Array[String] = [] for category in categories.keys(): result.append(category) result.sort() return result ## Get all unique set/opus prefixes from card IDs func get_card_sets() -> Array[String]: var sets: Dictionary = {} for card in _cards.values(): # Extract prefix before first dash (e.g., "1" from "1-001H") var dash_pos = card.id.find("-") if dash_pos > 0: var prefix = card.id.substr(0, dash_pos) sets[prefix] = true var result: Array[String] = [] for set_id in sets.keys(): result.append(set_id) # Sort numerically if possible result.sort_custom(func(a, b): var a_num = a.to_int() if a.is_valid_int() else 9999 var b_num = b.to_int() if b.is_valid_int() else 9999 return a_num < b_num ) return result ## Get card count func get_card_count() -> int: return _cards.size() ## Get or load a card texture func get_card_texture(card: CardData) -> Texture2D: if card.id in _card_textures: return _card_textures[card.id] if card.image_path.is_empty(): return null # Try source-cards directory first (primary location for card images) var texture_path = "res://source-cards/" + card.image_path if ResourceLoader.exists(texture_path): var texture = load(texture_path) _card_textures[card.id] = texture return texture # Fallback to assets/cards directory texture_path = "res://assets/cards/" + card.image_path if ResourceLoader.exists(texture_path): var texture = load(texture_path) _card_textures[card.id] = texture return texture return null ## Create a list of card IDs for a deck (for testing) ## Player 0 gets Fire/Ice deck, Player 1 gets Wind/Lightning deck func create_test_deck(player_index: int) -> Array[String]: var deck: Array[String] = [] # Define element pairs for each player var primary_element: Enums.Element var secondary_element: Enums.Element if player_index == 0: primary_element = Enums.Element.FIRE secondary_element = Enums.Element.ICE else: primary_element = Enums.Element.WIND secondary_element = Enums.Element.LIGHTNING # Get cards by element var primary_cards = get_cards_by_element(primary_element) var secondary_cards = get_cards_by_element(secondary_element) # Add 3 copies of each primary element card for card in primary_cards: for i in range(3): if deck.size() < 50: deck.append(card.id) # Add 3 copies of each secondary element card for card in secondary_cards: for i in range(3): if deck.size() < 50: deck.append(card.id) # If we still need cards, add from Earth/Water if deck.size() < 50: var filler_element = Enums.Element.EARTH if player_index == 0 else Enums.Element.WATER var filler_cards = get_cards_by_element(filler_element) for card in filler_cards: for i in range(3): if deck.size() < 50: deck.append(card.id) # Final fallback: add any cards if deck.size() < 50: var all_cards = get_all_cards() for card in all_cards: for i in range(3): if deck.size() < 50: deck.append(card.id) return deck ## Starter Deck Methods func _load_starter_decks() -> void: var file = FileAccess.open(STARTER_DECKS_PATH, FileAccess.READ) if not file: push_warning("Failed to open starter decks: " + STARTER_DECKS_PATH) return var json_text = file.get_as_text() file.close() var json = JSON.new() var error = json.parse(json_text) if error != OK: push_error("Failed to parse starter decks JSON: " + json.get_error_message()) return var data = json.get_data() if not data.has("starter_decks"): push_error("Starter decks file missing 'starter_decks' array") return for deck_data in data["starter_decks"]: var deck = StarterDeckData.new() deck.id = deck_data.get("id", "") deck.name = deck_data.get("name", "") deck.opus = deck_data.get("opus", "") deck.description = deck_data.get("description", "") deck.elements = deck_data.get("elements", []) deck.cards = deck_data.get("cards", []) deck.image = deck_data.get("image", "") _starter_decks.append(deck) print("CardDatabase: Loaded ", _starter_decks.size(), " starter decks") ## Get all starter decks func get_starter_decks() -> Array: return _starter_decks ## Get a starter deck by ID func get_starter_deck(deck_id: String) -> StarterDeckData: for deck in _starter_decks: if deck.id == deck_id: return deck return null ## Get a random starter deck func get_random_starter_deck() -> StarterDeckData: if _starter_decks.is_empty(): return null return _starter_decks[randi() % _starter_decks.size()] ## Data Classes class StarterDeckData: var id: String = "" var name: String = "" var opus: String = "" var description: String = "" var elements: Array = [] # Array of element name strings var cards: Array = [] # Array of card IDs var image: String = "" # Path to box art image func get_texture() -> Texture2D: if image.is_empty(): return null var texture_path = "res://assets/ui/" + image if ResourceLoader.exists(texture_path): return load(texture_path) return null class CardData: var id: String = "" var name: String = "" var type: Enums.CardType = Enums.CardType.FORWARD var elements: Array[Enums.Element] = [] var cost: int = 0 var power: int = 0 var job: String = "" var category: String = "" var is_generic: bool = false var has_ex_burst: bool = false var has_haste: bool = false var image_path: String = "" var abilities: Array[AbilityData] = [] func get_primary_element() -> Enums.Element: if elements.size() > 0: return elements[0] return Enums.Element.FIRE func is_multi_element() -> bool: return elements.size() > 1 ## Check if card has a specific ability by name (e.g., "Brave", "Haste", "First Strike") func has_ability(ability_name: String) -> bool: for ability in abilities: if ability.name.to_lower() == ability_name.to_lower(): return true return false class AbilityData: var type: Enums.AbilityType = Enums.AbilityType.FIELD var name: String = "" var effect: String = "" var trigger: String = "" var is_ex_burst: bool = false var cost: CostData = null class CostData: var generic: int = 0 var fire: int = 0 var ice: int = 0 var wind: int = 0 var lightning: int = 0 var water: int = 0 var earth: int = 0 var light: int = 0 var dark: int = 0 var requires_dull: bool = false var discard_count: int = 0 var specific_discard: String = "" func get_total_cp() -> int: return generic + fire + ice + wind + lightning + water + earth + light + dark func get_element_requirements() -> Dictionary: var reqs = {} if fire > 0: reqs[Enums.Element.FIRE] = fire if ice > 0: reqs[Enums.Element.ICE] = ice if wind > 0: reqs[Enums.Element.WIND] = wind if lightning > 0: reqs[Enums.Element.LIGHTNING] = lightning if water > 0: reqs[Enums.Element.WATER] = water if earth > 0: reqs[Enums.Element.EARTH] = earth if light > 0: reqs[Enums.Element.LIGHT] = light if dark > 0: reqs[Enums.Element.DARK] = dark return reqs