Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.

Expand Down
53 changes: 38 additions & 15 deletions card_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion cartas.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"text": "footer carta 1",
"color": "#333333"
},
"commandPoints": 12,
"commandPoints": "12/4",
"image": "BarcoPirata.jpg"
},
"Carta2": {
Expand Down
86 changes: 46 additions & 40 deletions draw_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 6 additions & 5 deletions layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions tests/test_card_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand Down