diff --git a/README.md b/README.md index 30349e7..089afd2 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ El archivo de definicion de cartas es un archivo en jormato json con el siguente "color": "#RRGGBB", "font_style": "normal | negrita | itálica" }, - "commandPoints": int, + "commandPoints": int | "X/Y", "power": int, "toughness": int, "image": "str", @@ -82,7 +82,7 @@ El archivo de definicion de cartas es un archivo en jormato json con el siguente 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. Puedes controlar el color de fondo del lienzo con el campo opcional `background_color`. Debe indicarse en formato hexadecimal (`#RRGGBB`) y solo se aplica cuando la carta no utiliza una imagen a pantalla completa (`full_frame_image: false`). -El valor `commandPoints` representa los puntos de mando de la carta. Siempre se mostrará como un número dentro de un escudo con borde negro en la esquina superior derecha. +El valor `commandPoints` representa los puntos de mando de la carta. Se mostrará en negrita dentro de un escudo más pequeño con borde negro en la esquina superior derecha. Cuando el valor tenga el formato `X/Y`, se dibujará un segundo escudo del mismo tamaño a la izquierda, con fondo negro y el valor `Y` en blanco y negrita. El bloque `footer` es opcional y permite mostrar una nota en la parte inferior de la carta. Puedes personalizar el texto, su color y el estilo de fuente (`normal`, `negrita` o `itálica`). Si no se especifica `font_style`, se utilizará `normal` por defecto. 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. Puedes utilizar el argumento `--output-dir` para indicar otro directorio base donde almacenar las cartas generadas, lo que facilita mantener varios prototipos separados. diff --git a/card_model.py b/card_model.py index 936a832..87e64fa 100644 --- a/card_model.py +++ b/card_model.py @@ -30,6 +30,7 @@ def __init__(self, name=None, db=None): self.cardText = "Some text" self.cardTextColour = "#000000" self.commandPoints = 0 + self.commandPointsSecondary = None self.power = None self.toughness = None self.image = None @@ -79,7 +80,9 @@ def load(self, data: dict): if command_points_value is None: command_points_value = data.get('manaCost') - self.commandPoints = self._parse_command_points(command_points_value) + primary, secondary = self._parse_command_points(command_points_value) + self.commandPoints = primary + self.commandPointsSecondary = secondary if 'power' in data: self.power = int(data['power']) @@ -119,7 +122,7 @@ def load(self, data: dict): self.footerFontStyle = self._normalise_footer_style(footer_style) def __str__(self): - return f'{self.headerText} - {self.commandPoints} ({self.typeStr})' + return f'{self.headerText} - {self.get_command_points_display()} ({self.typeStr})' def get_text_color_rgb(self): return self._hex_to_rgb(self.cardTextColour, default=(0.0, 0.0, 0.0)) @@ -180,23 +183,43 @@ def _normalise_footer_style(value: str) -> str: return style_map.get(normalised, 'normal') + def get_command_points_display(self) -> str: + if self.commandPointsSecondary is None: + return str(self.commandPoints) + + return f'{self.commandPoints}/{self.commandPointsSecondary}' + @staticmethod - def _parse_command_points(value) -> int: + def _parse_command_points(value): if value is None: - return 0 + return (0, None) if isinstance(value, (int, float)): - return int(value) + return (int(value), None) text = str(value).strip() if not text: - return 0 - - digits = ''.join(ch for ch in text if ch.isdigit() or ch == '-') - if not digits: - return 0 - - try: - return int(digits) - except ValueError: - return 0 + return (0, None) + + def _coerce(part, *, allow_none: bool = False): + digits = ''.join(ch for ch in part if ch.isdigit() or ch == '-') + if not digits: + return None if allow_none else 0 + + try: + return int(digits) + except ValueError: + return None if allow_none else 0 + + if '/' in text: + left, right = text.split('/', 1) + primary = _coerce(left) + secondary = _coerce(right, allow_none=True) + if primary is None: + primary = 0 + return (primary, secondary) + + primary = _coerce(text) + if primary is None: + primary = 0 + return (primary, None) diff --git a/cartas.json b/cartas.json index 3108cd7..e0ba43c 100644 --- a/cartas.json +++ b/cartas.json @@ -17,7 +17,7 @@ "text": "footer carta 1", "color": "#333333" }, - "commandPoints": 12, + "commandPoints": "12/4", "image": "BarcoPirata.jpg" }, "Carta2": { diff --git a/draw_card.py b/draw_card.py index 11545a7..51e749f 100644 --- a/draw_card.py +++ b/draw_card.py @@ -104,52 +104,58 @@ def drawCard( # Draw command points shield ctx.save() - shield_x, shield_y = layout.commandPointsShieldTL + base_shield_x, shield_y = layout.commandPointsShieldTL shield_width, shield_height = layout.commandPointsShieldSize point_height = layout.commandPointsShieldPointHeight + gap = getattr(layout, "commandPointsShieldGap", 0.0) - ctx.move_to(shield_x, shield_y) - ctx.line_to(shield_x + shield_width, shield_y) - ctx.line_to(shield_x + shield_width, shield_y + shield_height * 0.65) - ctx.line_to(shield_x + shield_width / 2.0, shield_y + shield_height + point_height) - ctx.line_to(shield_x, shield_y + shield_height * 0.65) - ctx.close_path() - - ctx.set_source_rgb(1.0, 1.0, 1.0) - ctx.fill_preserve() - ctx.set_line_width(layout.commandPointsBorderWidth) - ctx.set_source_rgb(0.0, 0.0, 0.0) - ctx.stroke() - - ctx.set_font_size(layout.commandPointsFontSize) - text = str(card.commandPoints) - extents = ctx.text_extents(text) - center_x = shield_x + shield_width / 2.0 - center_y = shield_y + (shield_height + point_height) / 2.0 - text_x = center_x - (extents.x_bearing + extents.width / 2.0) - text_y = center_y - (extents.y_bearing + extents.height / 2.0) - - ctx.set_source_rgb(0.0, 0.0, 0.0) - ctx.move_to(text_x, text_y) - ctx.show_text(text) - ctx.restore() + shield_values = [] + if card.commandPointsSecondary is not None: + secondary_x = base_shield_x - shield_width - gap + shield_values.append((card.commandPointsSecondary, secondary_x, True)) - # Draw footer text - if card.footerText: - footer_slant = cairo.FONT_SLANT_NORMAL - footer_weight = cairo.FONT_WEIGHT_NORMAL + shield_values.append((card.commandPoints, base_shield_x, False)) - if card.footerFontStyle == 'italic': - footer_slant = cairo.FONT_SLANT_ITALIC - if card.footerFontStyle == 'bold': - footer_weight = cairo.FONT_WEIGHT_BOLD + ctx.select_font_face('serif', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + for value, shield_x, invert in shield_values: + ctx.save() + ctx.move_to(shield_x, shield_y) + ctx.line_to(shield_x + shield_width, shield_y) + ctx.line_to(shield_x + shield_width, shield_y + shield_height * 0.65) + ctx.line_to(shield_x + shield_width / 2.0, shield_y + shield_height + point_height) + ctx.line_to(shield_x, shield_y + shield_height * 0.65) + ctx.close_path() + + if invert: + ctx.set_source_rgb(0.0, 0.0, 0.0) + ctx.fill_preserve() + ctx.set_line_width(layout.commandPointsBorderWidth) + ctx.set_source_rgb(0.0, 0.0, 0.0) + ctx.stroke() + text_color = (1.0, 1.0, 1.0) + else: + ctx.set_source_rgb(1.0, 1.0, 1.0) + ctx.fill_preserve() + ctx.set_line_width(layout.commandPointsBorderWidth) + ctx.set_source_rgb(0.0, 0.0, 0.0) + ctx.stroke() + text_color = (0.0, 0.0, 0.0) + + ctx.set_font_size(layout.commandPointsFontSize) + text = str(value) + extents = ctx.text_extents(text) + center_x = shield_x + shield_width / 2.0 + center_y = shield_y + (shield_height + point_height) / 2.0 + text_x = center_x - (extents.x_bearing + extents.width / 2.0) + text_y = center_y - (extents.y_bearing + extents.height / 2.0) + + ctx.set_source_rgb(*text_color) + ctx.move_to(text_x, text_y) + ctx.show_text(text) + ctx.restore() - ctx.set_source_rgb(*card.get_footer_text_color_rgb()) - ctx.set_font_size(layout.footerH) - ctx.select_font_face('serif', footer_slant, footer_weight) - ctx.move_to(*layout.footerBL) - ctx.show_text(card.footerText) - ctx.select_font_face('serif') + ctx.select_font_face('serif') + ctx.restore() # Draw footer text if card.footerText: diff --git a/layout.py b/layout.py index 5fb6b26..64f5888 100644 --- a/layout.py +++ b/layout.py @@ -36,11 +36,12 @@ SINGLE_CARD_DPI = 300 CARD_CORNER_RADIUS_MM = 3.0 -commandPointsShieldTL = (CARD_WIDTH_MM - 12.0, 4.0) -commandPointsShieldSize = (9.0, 9.5) -commandPointsShieldPointHeight = 3.0 -commandPointsFontSize = 4.0 -commandPointsBorderWidth = 0.5 +commandPointsShieldTL = (CARD_WIDTH_MM - 9.5, 4.0) +commandPointsShieldSize = (7.2, 8.0) +commandPointsShieldPointHeight = 2.4 +commandPointsFontSize = 3.6 +commandPointsBorderWidth = 0.45 +commandPointsShieldGap = 1.0 def mm_to_pixels(value_mm: float, dpi: float) -> int: diff --git a/tests/test_card_model.py b/tests/test_card_model.py index c0efea7..79c8e23 100644 --- a/tests/test_card_model.py +++ b/tests/test_card_model.py @@ -44,6 +44,8 @@ def test_load_with_optional_fields(self): self.assertEqual(card.cardTextColour, "#112233") self.assertEqual(card.get_text_color_rgb(), (0x11 / 255.0, 0x22 / 255.0, 0x33 / 255.0)) self.assertEqual(card.commandPoints, 5) + self.assertIsNone(card.commandPointsSecondary) + self.assertEqual(card.get_command_points_display(), "5") self.assertEqual(card.power, 2) self.assertEqual(card.toughness, 3) self.assertEqual(card.image, "wizard.png") @@ -73,6 +75,8 @@ def test_load_with_defaults(self): self.assertEqual(card.cardTextColour, "#000000") self.assertEqual(card.get_text_color_rgb(), (0.0, 0.0, 0.0)) self.assertEqual(card.commandPoints, 0) + self.assertIsNone(card.commandPointsSecondary) + self.assertEqual(card.get_command_points_display(), "0") self.assertIsNone(card.power) self.assertIsNone(card.toughness) self.assertIsNone(card.image) @@ -111,6 +115,7 @@ def test_load_legacy_mana_cost_field(self): card.load(data) self.assertEqual(card.commandPoints, 12) + self.assertIsNone(card.commandPointsSecondary) def test_load_coerces_string_command_points(self): data = { @@ -125,6 +130,23 @@ def test_load_coerces_string_command_points(self): card.load(data) self.assertEqual(card.commandPoints, 8) + self.assertIsNone(card.commandPointsSecondary) + + def test_load_parses_split_command_points(self): + data = { + "header": { + "text": "Split", + }, + "type": "Sorcery", + "commandPoints": "3/7", + } + + card = CardModel() + card.load(data) + + self.assertEqual(card.commandPoints, 3) + self.assertEqual(card.commandPointsSecondary, 7) + self.assertEqual(card.get_command_points_display(), "3/7") def test_load_supports_legacy_name_field(self): data = {