From 2e5d645f5d3af3b7d89f0a28afd6a9cd271cb4c9 Mon Sep 17 00:00:00 2001 From: fmolinagomez <33826980+fmolinagomez@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:19:07 +0200 Subject: [PATCH] Remove deck CSV workflow --- LWCProto.py | 276 +++++++++++---------------------------- README.md | 42 +++--- add_images.py | 49 ++----- card_model.py | 93 +++++++++++-- cartas.json | 50 ++++--- draw_card.py | 36 +++-- tests/test_card_model.py | 73 ++++++++++- 7 files changed, 318 insertions(+), 301 deletions(-) diff --git a/LWCProto.py b/LWCProto.py index 875ce1a..fb8ae6a 100644 --- a/LWCProto.py +++ b/LWCProto.py @@ -1,12 +1,9 @@ #! /usr/bin/env python3 -import csv -import os +import argparse import re -import numpy as np +from pathlib import Path import cairo -import argparse - import layout from draw_card import drawCard @@ -16,219 +13,100 @@ def extant_file(x): - """ - 'Type' for argparse - checks that file exists but does not open. - """ - if not os.path.exists(x): - # Argparse uses the ArgumentTypeError to give a rejection message like: - # error: argument input: x does not exist - raise argparse.ArgumentTypeError("{0} does not exist".format(x)) - return x + """Argparse helper that ensures a file exists without opening it.""" + path = Path(x) + if not path.exists(): + raise argparse.ArgumentTypeError(f"{x} does not exist") + return str(path) ##### CLI args ##### -parser = argparse.ArgumentParser(description="Deck Generator for Game Designers") -parser.add_argument('-d', '--deck', type=extant_file, help='csv file containing the deck', metavar="FILE") -parser.add_argument('-c', '--cards', type=extant_file, help='json file containing cards description', metavar="FILE", required=True) - -parser.add_argument('-i', '--images', help='Add images to cards', action='store_true') +parser = argparse.ArgumentParser(description="Card generator for game prototypes") parser.add_argument( - '--full-frame-images', - help='Scale images to cover the entire card and adjust text colour for contrast', - action='store_true', + '-c', + '--cards', + type=extant_file, + help='json file containing cards description', + metavar='FILE', + required=True, ) -parser.add_argument('-r', '--rgb', help='Update layout card border colour with given R,G,B, only works with default layout', nargs=3, type=int) -parser.add_argument('-l', '--layout', help='Use a different layout than default', type=extant_file, metavar="FILE") -parser.add_argument('--single-card', help='Render each card as an individual 63x85mm PNG at 300 DPI', action='store_true') +parser.add_argument('-i', '--images', help='Add images to cards', action='store_true') args = parser.parse_args() handle_images = args.images -full_frame_images = args.full_frame_images -modify_layout = args.rgb cards_file = args.cards -single_card_mode = args.single_card -deck_file = args.deck - -if single_card_mode and deck_file is not None: - parser.error('the --single-card option cannot be used together with --deck/-d') - -if (not single_card_mode) and deck_file is None: - parser.error('the --deck/-d option is required unless --single-card is specified') - -if full_frame_images and not handle_images: - parser.error('the --full-frame-images option requires --images') cards = CardDeck(cards_file) -nameList = [] -list_copy = [] +cardList = [] +for entry in cards.getDb().values(): + card = CardModel() + card.load(entry) + cardList.append(card) -if single_card_mode: - deck_name = os.path.splitext(os.path.basename(cards_file))[0] - cardList = [] - for entry in cards.getDb().values(): - card = CardModel() - card.load(entry) - cardList.append(card) -else: - deck_name = os.path.basename(deck_file)[:-4] - with open(deck_file, encoding='utf-8') as csvFile: - reader = csv.reader(csvFile) - list_copy.append(reader.__next__()) - for row in reader: - list_copy.append(row) - nameList = nameList + [row[1]] * int(row[0]) - - cardList = [CardModel(name, cards.getDb()) for name in nameList] - pageList = [cardList[i:i+9] for i in range(0, len(cardList), 9)] - -if (handle_images and not full_frame_images) or (modify_layout is not None): - from add_images import BaseImage +output_root = Path('output') / Path(cards_file).stem +cards_output_dir = output_root / 'cards' +cards_output_dir.mkdir(parents=True, exist_ok=True) -if handle_images and not full_frame_images: +if handle_images: + from add_images import BaseImage from add_images import addImage from add_images import processImage - -if handle_images and full_frame_images: from add_images import load_full_frame_surface -if not os.path.exists('decks'): - os.mkdir('decks') -if not os.path.exists(os.path.join('decks',deck_name)): - os.mkdir(os.path.join('decks',deck_name)) - -if single_card_mode: - cards_output_dir = os.path.join('decks', deck_name, 'cards') - os.makedirs(cards_output_dir, exist_ok=True) - single_dpi = layout.SINGLE_CARD_DPI - - def _slugify(value: str) -> str: - value = value.strip() - value = re.sub(r'\s+', '_', value) - value = re.sub(r'[^A-Za-z0-9_-]', '', value) - return value or 'card' - - for index, card in enumerate(cardList): - print(f'Card {index}: {card}') - surf = layout.get_single_card_surface(single_dpi) - ctx = cairo.Context(surf) - - card_matrix = layout.get_single_card_matrix(single_dpi) - ctx.set_matrix(card_matrix) - layout.clip_card(ctx) - ctx.set_source_rgb(1, 1, 1) - ctx.paint() - - - text_color = (0.0, 0.0, 0.0) - if handle_images and full_frame_images: - full_frame_surface, computed_color = load_full_frame_surface(card, single_dpi) - if full_frame_surface is not None: - ctx.save() - ctx.identity_matrix() - ctx.set_source_surface(full_frame_surface, 0, 0) - ctx.paint() - ctx.restore() - if computed_color is not None: - text_color = computed_color - - ctx.reset_clip() - ctx.set_matrix(card_matrix) - drawCard(card, ctx, text_color=text_color) - card_filename = f"{index:03d}_{_slugify(card.nameStr)}.png" - output_path = os.path.join(cards_output_dir, card_filename) - surf.write_to_png(output_path) - - if handle_images and not full_frame_images: - processImage(card, deck_name, dpi=single_dpi) - baseImage = BaseImage(output_path) - updated_image = addImage(card, baseImage, deck_name, dpi=single_dpi) - baseImage.update(updated_image) - baseImage.save(output_path) + image_output_dir = output_root / 'images' else: - for page_number in range(len(pageList)): - print(f'Page {page_number}:') - page = pageList[page_number] - surf = layout.getSurface() - ctx = cairo.Context(surf) - - page_dpi = layout.get_surface_dpi(surf) - - for i in range(len(page)): - card = page[i] - cardPos = (i % 3, i // 3) - print(cardPos) - print(card) - - text_color = (0.0, 0.0, 0.0) - if handle_images and full_frame_images: - full_frame_surface, computed_color = load_full_frame_surface(card, page_dpi) - if full_frame_surface is not None: - card_origin_mm = layout.get_card_origin_mm(cardPos) - origin_px = layout.pair_mm_to_pixels( - card_origin_mm, - page_dpi, - ) - ctx.save() - ctx.identity_matrix() - layout.clip_card_absolute(ctx, card_origin_mm, page_dpi) - ctx.set_source_surface(full_frame_surface, *origin_px) - ctx.paint() - ctx.restore() - if computed_color is not None: - text_color = computed_color - - mat = layout.getMatrix(*cardPos, surf) - ctx.set_matrix(mat) - drawCard(card, ctx, text_color=text_color) - - output_path = f'decks/{deck_name}/{deck_name}_p{page_number}.png' - surf.write_to_png(output_path) - - if (modify_layout is not None): - from PIL import Image - - baseImage = BaseImage(output_path) - temp = baseImage.baseImage.convert('RGBA') - data = np.array(temp) - red, green, blue, alpha = data.T - for i in range(0,63): - white_areas = (red == 190+i) & (blue == 190+i) & (green == 190+i) - data[..., :-1][white_areas.T] = (modify_layout[0], modify_layout[1], modify_layout[2]) - baseImage.update(Image.fromarray(data)) - baseImage.save(output_path) - - - #import pdb;pdb.set_trace() - if handle_images and not full_frame_images: - page_dpi = layout.get_surface_dpi(surf) - baseImage = BaseImage(output_path) - for i in range (len(page)): - card = page[i] - cardPos = (i % 3, i // 3) - card_origin_mm = layout.get_card_origin_mm(cardPos) - image_position_mm = ( - card_origin_mm[0] + layout.ART_OFFSET_MM[0], - card_origin_mm[1] + layout.ART_OFFSET_MM[1], - ) - processImage(card,deck_name, dpi=page_dpi) - baseImage.update( - addImage( - card, - baseImage, - deck_name, - position_mm=image_position_mm, - dpi=page_dpi, - ) - ) - baseImage.save(output_path) - - -if not single_card_mode: - with open(f'decks/{deck_name}/{deck_name}.csv', 'w') as deck_copy: - filewriter = csv.writer(deck_copy) - for element in list_copy: - filewriter.writerow(element) + image_output_dir = None + +single_dpi = layout.SINGLE_CARD_DPI + + +def _slugify(value: str) -> str: + value = value.strip() + value = re.sub(r'\s+', '_', value) + value = re.sub(r'[^A-Za-z0-9_-]', '', value) + return value or 'card' + + +for index, card in enumerate(cardList): + print(f'Card {index}: {card}') + surf = layout.get_single_card_surface(single_dpi) + ctx = cairo.Context(surf) + + card_matrix = layout.get_single_card_matrix(single_dpi) + ctx.set_matrix(card_matrix) + layout.clip_card(ctx) + ctx.set_source_rgb(1, 1, 1) + ctx.paint() + + if handle_images and card.imageFullFrame: + full_frame_surface = load_full_frame_surface(card, single_dpi) + if full_frame_surface is not None: + ctx.save() + ctx.identity_matrix() + ctx.set_source_surface(full_frame_surface, 0, 0) + ctx.paint() + ctx.restore() + + ctx.reset_clip() + ctx.set_matrix(card_matrix) + drawCard(card, ctx) + + card_filename = f"{index:03d}_{_slugify(card.headerText)}.png" + output_path = cards_output_dir / card_filename + surf.write_to_png(str(output_path)) + + if handle_images and not card.imageFullFrame: + processImage(card, output_dir=image_output_dir, dpi=single_dpi) + baseImage = BaseImage(str(output_path)) + updated_image = addImage( + card, + baseImage, + output_dir=image_output_dir, + dpi=single_dpi, + ) + baseImage.update(updated_image) + baseImage.save(str(output_path)) diff --git a/README.md b/README.md index 7abe680..5a82d1b 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,14 @@ En principio todo lo que se necesita para ejecutar este script es python 3.9 ins ### Sintaxis ``` -usage: LWCProto.py [-h] -d FILE -c FILE [-i] [-r RGB RGB RGB] [-l FILE] +usage: LWCProto.py [-h] -c FILE [-i] -Deck Generator for Game Designers +Card generator for game prototypes -optional arguments: +options: -h, --help show this help message and exit - -d FILE, --deck FILE csv file containing the deck - -c FILE, --cards FILE - json file containing cards description + -c FILE, --cards FILE json file containing cards description -i, --images Add images to cards - -r RGB RGB RGB, --rgb RGB RGB RGB - Update layout card border colour with given R,G,B, only works with default layout - -l FILE, --layout FILE (==> Not ready yet) - Use a different layout than default ``` ### Archivo de definicion de cartas: @@ -48,28 +42,30 @@ El archivo de definicion de cartas es un archivo en jormato json con el siguente { "CartID": { - "name": "str", + "header": { + "text": "str", + "color": "#RRGGBB", + "banner": true | false, + "banner_color": "#RRGGBB" + }, "type": "str", "subtype": "str", - "text": "str", + "card_text": { + "text": "str", + "colour": "#RRGGBB" + }, "manaCost": "str", "power": int, "toughness": int, - "image": "str" + "image": "str", + "full_frame_image": true | false }, .... } ``` -Las imagenes deben almacenarse en el directorio "images" que se encuentra en la misma carpeta que LWCProto.py, el formato de las imagenes es indiferente y su tamaño tambien estas seran redimensionadas automaticamente para adaptarse al tamaño disponible en el layout - -### Archivo de definicion del mazo - -Archivo en formato csv que tiene el siguiente formato: -``` -Qty,Name -40,CartID_1 -300,CartID_2 -``` +El objeto `header` define el texto visible en la parte superior de la carta. El campo `color` ajusta el color del texto, mientras que los campos `banner` y `banner_color` permiten activar un recuadro de color sólido detrás del encabezado cuando sea necesario. +El bloque `card_text` permite especificar el texto del cuerpo y el color con el que debe renderizarse. Para las imágenes puedes indicar un nombre de archivo directamente o un objeto con las claves `source` y `full_frame`. Cuando `full_frame_image` (o `full_frame` en el objeto de imagen) es `true`, la ilustración se ampliará para cubrir toda la carta; en caso contrario se mantendrá dentro del marco de arte. +Las imagenes deben almacenarse en el directorio "images" que se encuentra en la misma carpeta que LWCProto.py, el formato de las imagenes es indiferente y su tamaño tambien estas seran redimensionadas automaticamente para adaptarse al tamaño disponible en el layout. El script genera los resultados dentro del directorio `output//cards`, creando versiones individuales en PNG de cada carta del archivo JSON. Si se habilitan las imágenes (`--images`), también se creará una carpeta `output//images` con las versiones redimensionadas de las ilustraciones. ## Fuentes Este proyecto se basa en el original [LightWeithgMTGProxy](https://github.com/tilleraj/LightWeightMTGProxy) todo el credito le pertenece a él, hacemos extensivo sus agradecimientos a .Rai de Cardgame Coalition creador del layout original que estamos utilizando diff --git a/add_images.py b/add_images.py index 410e72a..c981bb6 100644 --- a/add_images.py +++ b/add_images.py @@ -7,15 +7,15 @@ import card_model import layout -from PIL import Image, ImageStat +from PIL import Image try: _RESAMPLE = Image.Resampling.LANCZOS except AttributeError: _RESAMPLE = Image.LANCZOS -def _ensure_output_dir(deck: str) -> pathlib.Path: - path = pathlib.Path('decks') / deck / 'images' +def _ensure_output_dir(path: pathlib.Path) -> pathlib.Path: + path = pathlib.Path(path) path.mkdir(parents=True, exist_ok=True) return path @@ -51,8 +51,8 @@ def _load_resized_source_image(image_name: str, size_px): def processImage( card: card_model.CardModel, - deck: str, *, + output_dir: pathlib.Path, size_mm=layout.ART_SIZE_MM, dpi: int = layout.SINGLE_CARD_DPI, ): @@ -60,7 +60,7 @@ def processImage( return size_px = layout.pair_mm_to_pixels(size_mm, dpi) - output_dir = _ensure_output_dir(deck) + output_dir = _ensure_output_dir(output_dir) destination = output_dir / str(card.image) if destination.exists(): @@ -82,8 +82,8 @@ def processImage( def addImage( card: card_model.CardModel, base: BaseImage, - deck: str, *, + output_dir: pathlib.Path, position_mm=None, size_mm=layout.ART_SIZE_MM, dpi: int = layout.SINGLE_CARD_DPI, @@ -92,7 +92,7 @@ def addImage( if card.image is None: return base.get() - output_dir = _ensure_output_dir(deck) + output_dir = _ensure_output_dir(output_dir) image_path = output_dir / str(card.image) try: @@ -119,50 +119,23 @@ def addImage( return image_copy -def _relative_luminance_from_mean(mean_rgb): - def _srgb_to_linear(value): - if value <= 0.04045: - return value / 12.92 - return ((value + 0.055) / 1.055) ** 2.4 - - red, green, blue = (channel / 255.0 for channel in mean_rgb) - red_lin = _srgb_to_linear(red) - green_lin = _srgb_to_linear(green) - blue_lin = _srgb_to_linear(blue) - return 0.2126 * red_lin + 0.7152 * green_lin + 0.0722 * blue_lin - - -def _best_text_color(image: Image.Image): - mean_rgb = ImageStat.Stat(image.convert('RGB')).mean - luminance = _relative_luminance_from_mean(mean_rgb) - - contrast_with_white = (1.05) / (luminance + 0.05) - contrast_with_black = (luminance + 0.05) / 0.05 - - if contrast_with_white >= contrast_with_black: - return (1.0, 1.0, 1.0) - return (0.0, 0.0, 0.0) - - @lru_cache(maxsize=128) def _load_full_frame_surface_cached(image_name: str, dpi: int): size_px = layout.pair_mm_to_pixels((layout.CARD_WIDTH_MM, layout.CARD_HEIGHT_MM), dpi) resized_image = _load_resized_source_image(image_name, size_px) if resized_image is None: - return None, None - - text_color = _best_text_color(resized_image) + return None buffer = io.BytesIO() resized_image.save(buffer, format='PNG') buffer.seek(0) surface = cairo.ImageSurface.create_from_png(buffer) - return surface, text_color + return surface def load_full_frame_surface(card: card_model.CardModel, dpi: int): - if card.image is None: - return None, None + if (card.image is None) or (not getattr(card, "imageFullFrame", False)): + return None return _load_full_frame_surface_cached(str(card.image), dpi) diff --git a/card_model.py b/card_model.py index 835d3d3..a0bb756 100644 --- a/card_model.py +++ b/card_model.py @@ -22,40 +22,117 @@ def getDb(self): class CardModel: def __init__(self, name=None, db=None): - self.nameStr = "NAME" + self.headerText = "HEADER" + self.headerColour = "#000000" + self.headerBanner = False + self.headerBannerColour = "#FFFFFF" self.typeStr = "TYPE - SUBTYPE" self.cardText = "Some text" + self.cardTextColour = "#000000" self.manaCost = "\{W\}" self.power = None self.toughness = None self.image = None + self.imageFullFrame = False if (name is not None) and (db is not None): # self.load(db[name][0]) For magic AllCards need this index self.load(db[name]) def load(self, data: dict): - self.nameStr = data['name'] + header = data.get('header') + if header is not None: + header = header or {} + self.headerText = header.get('text', '') or '' + self.headerColour = header.get('color', '#000000') or '#000000' + self.headerBanner = bool(header.get('banner', False)) + self.headerBannerColour = header.get('banner_color', '#FFFFFF') or '#FFFFFF' + else: + name_value = data.get('name', '') or '' + self.headerText = name_value + self.headerColour = '#000000' + self.headerBanner = False + self.headerBannerColour = '#FFFFFF' + self.typeStr = data['type'] if ('subtype' in data): self.typeStr = self.typeStr + " - " + data['subtype'] - if ('text' in data): + if 'card_text' in data: + card_text = data['card_text'] or {} + self.cardText = card_text.get('text', '') + self.cardTextColour = card_text.get('colour', '#000000') or '#000000' + elif 'text' in data: + # Backwards compatibility with older card definitions. self.cardText = data['text'] + self.cardTextColour = '#000000' else: self.cardText = "" - + self.cardTextColour = '#000000' + if ('manaCost' in data): self.manaCost = data['manaCost'] else: self.manaCost = "" - + if 'power' in data: self.power = int(data['power']) self.toughness = int(data['toughness']) - if 'image' in data: - self.image = data['image'] + else: + self.power = None + self.toughness = None + + image_value = data.get('image') + image_full_frame = False + + if isinstance(image_value, dict): + self.image = image_value.get('source') + image_full_frame = bool(image_value.get('full_frame', False)) + else: + self.image = image_value + image_full_frame = bool(data.get('full_frame_image', False)) + + if not self.image: + self.image = None + image_full_frame = False + + self.imageFullFrame = image_full_frame def __str__(self): - return f'{self.nameStr} - {self.manaCost} ({self.typeStr})' + return f'{self.headerText} - {self.manaCost} ({self.typeStr})' + + def get_text_color_rgb(self): + return self._hex_to_rgb(self.cardTextColour, default=(0.0, 0.0, 0.0)) + + def get_header_text_color_rgb(self): + return self._hex_to_rgb(self.headerColour, default=(0.0, 0.0, 0.0)) + + def get_header_banner_color_rgb(self): + return self._hex_to_rgb(self.headerBannerColour, default=(1.0, 1.0, 1.0)) + + @staticmethod + def _hex_to_rgb(colour: str, *, default): + value = (colour or '').strip() + if value.startswith('#'): + value = value[1:] + + if len(value) != 6: + return default + + try: + red = int(value[0:2], 16) / 255.0 + green = int(value[2:4], 16) / 255.0 + blue = int(value[4:6], 16) / 255.0 + except ValueError: + return default + + return (red, green, blue) + + @property + def nameStr(self): + return self.headerText + + @nameStr.setter + def nameStr(self, value): + self.headerText = value or '' diff --git a/cartas.json b/cartas.json index 5ec758d..ff4c225 100644 --- a/cartas.json +++ b/cartas.json @@ -1,23 +1,37 @@ { - "Carta1": - { - "name": "nombre 1", - "type": "Evento", - "subtype": "Obligatorio", + "Carta1": { + "header": { + "text": "nombre 1", + "color": "#000000", + "banner": false, + "banner_color": "#FFFFFF" + }, + "type": "Evento", + "subtype": "Obligatorio", + "card_text": { "text": "este es un texto de prueba", - "manaCost": "5/6 * {r}", - "image": "BarcoPirata.jpg" + "colour": "#000000" + }, + "manaCost": "5/6 * {r}", + "image": "BarcoPirata.jpg" + }, + "Carta2": { + "header": { + "text": "carta que te cagas", + "color": "#FFFFFF", + "banner": true, + "banner_color": "#000000" }, - - "Carta2": - { - "name": "carta que te cagas", - "type": "Reaccion", - "subtype": "combate", + "type": "Reaccion", + "subtype": "combate", + "card_text": { "text": "este es un texto de prueba para esta supercarta", - "manaCost": "2", - "power": 60, - "toughness": 9, - "image": "MinionNapoleonics.jpg" - } + "colour": "#FFFFFF" + }, + "manaCost": "2", + "power": 60, + "toughness": 9, + "image": "MinionNapoleonics.jpg", + "full_frame_image": true + } } diff --git a/draw_card.py b/draw_card.py index f3f2b3a..35847cb 100644 --- a/draw_card.py +++ b/draw_card.py @@ -40,8 +40,6 @@ def showWrappedText( def drawCard( card: card_model.CardModel, ctx: cairo.Context, - *, - text_color=(0.0, 0.0, 0.0), ): ctx.save() @@ -50,20 +48,40 @@ def drawCard( ctx.select_font_face('serif') - # Draw name - ctx.set_source_rgb(*text_color) + header_color = card.get_header_text_color_rgb() + body_color = card.get_text_color_rgb() + ctx.set_font_size(layout.nameH) + + if card.headerBanner: + header_text = card.headerText or '' + text_extents = ctx.text_extents(header_text) + padding = 1.0 + banner_top = layout.nameBL[1] + text_extents.y_bearing - padding + banner_height = text_extents.height + padding * 2 + if banner_height <= 0: + banner_height = layout.nameH + padding * 2 + banner_top = max(banner_top, 0.0) + + ctx.save() + ctx.set_source_rgb(*card.get_header_banner_color_rgb()) + ctx.rectangle(0, banner_top, layout.CARD_WIDTH_MM, banner_height) + ctx.fill() + ctx.restore() + + # Draw header text + ctx.set_source_rgb(*header_color) ctx.move_to(*layout.nameBL) - ctx.show_text(card.nameStr) + ctx.show_text(card.headerText) # Draw type - ctx.set_source_rgb(*text_color) + ctx.set_source_rgb(*body_color) ctx.set_font_size(layout.typeH) ctx.move_to(*layout.typeBL) ctx.show_text(card.typeStr) # Draw cardText - ctx.set_source_rgb(*text_color) + ctx.set_source_rgb(*body_color) ctx.set_font_size(layout.cardTextH) showWrappedText(ctx, card.cardText, top=layout.cardTextBL[1], @@ -75,13 +93,13 @@ def drawCard( # Draw power/toughness if card.power is not None: ptStr = str(card.power) + '/' + str(card.toughness) - ctx.set_source_rgb(*text_color) + ctx.set_source_rgb(*body_color) ctx.set_font_size(layout.ptH) ctx.move_to(*layout.ptBL) ctx.show_text(ptStr) # Draw Mana Cost - ctx.set_source_rgb(*text_color) + ctx.set_source_rgb(*body_color) ctx.set_font_size(layout.nameH) ctx.move_to( layout.manaRight - ctx.text_extents(card.manaCost).width, diff --git a/tests/test_card_model.py b/tests/test_card_model.py index e2e924a..20a800e 100644 --- a/tests/test_card_model.py +++ b/tests/test_card_model.py @@ -9,50 +9,111 @@ class CardModelLoadTest(unittest.TestCase): def test_load_with_optional_fields(self): data = { - "name": "Test Name", + "header": { + "text": "Test Name", + "color": "#ABCDEF", + "banner": True, + "banner_color": "#445566", + }, "type": "Creature", "subtype": "Wizard", - "text": "Draw a card", + "card_text": { + "text": "Draw a card", + "colour": "#112233", + }, "manaCost": "{1}{U}", "power": 2, "toughness": 3, "image": "wizard.png", + "full_frame_image": True, } card = CardModel() card.load(data) + self.assertEqual(card.headerText, "Test Name") self.assertEqual(card.nameStr, "Test Name") + self.assertEqual(card.headerColour, "#ABCDEF") + self.assertTrue(card.headerBanner) + self.assertEqual(card.headerBannerColour, "#445566") + self.assertEqual(card.get_header_text_color_rgb(), (0xAB / 255.0, 0xCD / 255.0, 0xEF / 255.0)) + self.assertEqual(card.get_header_banner_color_rgb(), (0x44 / 255.0, 0x55 / 255.0, 0x66 / 255.0)) self.assertEqual(card.typeStr, "Creature - Wizard") self.assertEqual(card.cardText, "Draw a card") + self.assertEqual(card.cardTextColour, "#112233") + self.assertEqual(card.get_text_color_rgb(), (0x11 / 255.0, 0x22 / 255.0, 0x33 / 255.0)) self.assertEqual(card.manaCost, "{1}{U}") self.assertEqual(card.power, 2) self.assertEqual(card.toughness, 3) self.assertEqual(card.image, "wizard.png") + self.assertTrue(card.imageFullFrame) - def test_load_without_optional_fields(self): + def test_load_with_defaults(self): data = { - "name": "Vanilla", + "header": { + "text": "Vanilla", + }, "type": "Creature", } card = CardModel() card.load(data) - self.assertEqual(card.nameStr, "Vanilla") + self.assertEqual(card.headerText, "Vanilla") + self.assertFalse(card.headerBanner) + self.assertEqual(card.headerColour, "#000000") + self.assertEqual(card.headerBannerColour, "#FFFFFF") + self.assertEqual(card.get_header_text_color_rgb(), (0.0, 0.0, 0.0)) + self.assertEqual(card.get_header_banner_color_rgb(), (1.0, 1.0, 1.0)) self.assertEqual(card.typeStr, "Creature") self.assertEqual(card.cardText, "") + self.assertEqual(card.cardTextColour, "#000000") + self.assertEqual(card.get_text_color_rgb(), (0.0, 0.0, 0.0)) self.assertEqual(card.manaCost, "") self.assertIsNone(card.power) self.assertIsNone(card.toughness) self.assertIsNone(card.image) + self.assertFalse(card.imageFullFrame) + + def test_load_supports_image_object(self): + data = { + "header": { + "text": "Object Image", + }, + "type": "Sorcery", + "image": { + "source": "object.png", + "full_frame": True, + }, + } + + card = CardModel() + card.load(data) + + self.assertEqual(card.image, "object.png") + self.assertTrue(card.imageFullFrame) + + def test_load_supports_legacy_name_field(self): + data = { + "name": "Legacy", + "type": "Enchantment", + } + + card = CardModel() + card.load(data) + + self.assertEqual(card.headerText, "Legacy") + self.assertEqual(card.headerColour, "#000000") + self.assertEqual(card.typeStr, "Enchantment") class CardDeckLoadTest(unittest.TestCase): def test_load_uses_provided_path(self): cards = { "Example": { - "name": "Example", + "header": { + "text": "Example", + }, "type": "Artifact", } }