505 lines
14 KiB
GDScript
505 lines
14 KiB
GDScript
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.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 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
|
|
|
|
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
|