from __future__ import annotations import json import logging import os import re import requests from .carddb import CardDB from .cards import Cards from .code import Code from .language import Language from .utils import CARD_BACK_URL, DECKS_DIR_NAME class TTSDeck(Cards): def __init__(self, codes: list[Code], name: str, description: str, face_down: bool): logger = logging.getLogger(__name__) super().__init__(name) self.__description = description self.__face_down = face_down # get cards from carddb carddb = CardDB() # non-imported cards codes_invalid = frozenset([ code for code in codes if code not in carddb ]) # show errors and remove non-imported cards for code in codes_invalid: logger.error(f"Code '{code}' not in CardDB, ignoring!") while code in codes: codes.remove(code) # put existing cards into deck self.extend([ carddb[code] for code in codes ]) @property def file_name(self) -> str: return f"{super().file_name}.json" __FFDECKS_API_URL = "https://ffdecks.com/api/deck" __RE_FFDECKS_ID = re.compile(r"((https?://)?ffdecks\.com(/+api)?/+deck/+)?([0-9]+).*", flags=re.UNICODE) @classmethod def from_ffdecks_deck(cls, deck_id: str) -> TTSDeck: logger = logging.getLogger(__name__) # check deck id match = TTSDeck.__RE_FFDECKS_ID.match(deck_id) if match is None: logger.error("Malformed Deck ID for FFDecks API!") return cls([], "", "", True) else: # extract deck id from match deck_id = match.groups()[3] # api request res = requests.get(TTSDeck.__FFDECKS_API_URL, params={"deck_id": deck_id}) if not res.ok: logger.error("Invalid Deck ID for FFDecks API!") return cls([], "", "", True) logger.info(f"Importing Deck {deck_id}") # pre-extract the used data deck_cards = [{ "code": card["card"]["serial_number"], "type": card["card"]["type"], "cost": int(card["card"]["cost"]), "count": int(card["quantity"]), } for card in res.json()["cards"]] # sort cards by type, then by cost def by_type(data: dict[str, str | int]) -> int: key_prios = { "Forward": 1, "Summon": 2, "Monster": 3, "Backup": 5, } if data["type"] in key_prios: return key_prios[data["type"]] else: return 4 def by_cost(data: dict[str, str | int]) -> int: return data["cost"] deck_cards.sort(key=by_cost) deck_cards.sort(key=by_type) # ffdecks quirk: some full-art promos in database replace_full_arts = { # line format: # full-art-id: normal id, "PR-051": "11-083", "PR-055": "11-062", } # replace with normal-art cards for card in deck_cards: if card["code"] in replace_full_arts: card["code"] = replace_full_arts[card["code"]] codes = [ # create list of code objects Code(card["code"]) # for each card for card in deck_cards # repeat to meet count for _ in range(card["count"]) ] # general metadata name = f"{res.json()['name']}" description = deck_id # create deck object return cls(codes, name, description, True) def get_tts_object(self, language: Language) -> dict[str, any]: carddb = CardDB() # unique face urls used unique_faces = set([ card[language].face for card in self ]) # lookup for indices of urls face_indices = { url: i + 1 for i, url in enumerate(unique_faces) } # build the "CustomDeck" dictionary custom_deck = { str(i): { "NumWidth": "10", "NumHeight": "7", "FaceURL": carddb.get_face_url(face), "BackURL": CARD_BACK_URL, } for face, i in face_indices.items() } # values both in main deck and each contained card common_dict = { "Transform": { "scaleX": 2.17822933, "scaleY": 1.0, "scaleZ": 2.17822933, "rotY": 180.0, }, "Locked": False, "Grid": True, "Snap": True, "Autoraise": True, "Sticky": True, "Tooltip": True, "GridProjection": False, } # cards contained in deck contained_objects = [ { "Nickname": card[language].name, "Description": card[language].text, "CardID": 100 * face_indices[card[language].face] + card.index, "Name": "Card", "Hands": True, "SidewaysCard": False, } | common_dict for card in self ] # extract the card ids deck_ids = [ contained_object["CardID"] for contained_object in contained_objects ] # create the deck dictionary deck_dict = {"ObjectStates": [ { "Nickname": self.name, "Description": self.__description, "DeckIDs": deck_ids, "CustomDeck": custom_deck, "ContainedObjects": contained_objects, "Name": "Deck", "Hands": False, "SidewaysCard": False, } | common_dict ]} if self.__face_down: # flip the deck deck_dict["ObjectStates"][0]["Transform"]["rotZ"] = 180.0 return deck_dict def get_json(self, language: Language) -> str: return json.dumps(self.get_tts_object(language), indent=2) def save(self, language: Language) -> None: # only save if the deck contains cards if self: if not os.path.exists(DECKS_DIR_NAME): os.mkdir(DECKS_DIR_NAME) with open(os.path.join(DECKS_DIR_NAME, self.file_name), "w") as file: file.write(self.get_json(language))