Author SHA1 Message Date
Erzangel f7409fe7c3 fix: image adjustments, flavor text font size
release-tag / release-image (push) Has been cancelled
2026-03-21 18:04:02 +01:00
Erzangel 79abb8452c feat: flavor text, new card skins, gitignore updates
release-tag / release-image (push) Has been cancelled
2026-03-21 17:38:01 +01:00
Erzangel 569aa630a2 feat(wip): flavor text sample
release-tag / release-image (push) Has been cancelled
2026-03-20 17:07:10 +01:00
Erzangel 4da86dd40f docs: update todo list
release-tag / release-image (push) Has been cancelled
2026-03-15 20:13:17 +01:00
Erzangel 151aaadb3f docs: push todo list
release-tag / release-image (push) Has been cancelled
2026-03-15 19:57:50 +01:00
Erzangel c368536d79 feat: add svg to png conversion capabilities
release-tag / release-image (push) Has been cancelled
2026-03-15 17:51:37 +01:00
Erzangel 8a024929bc feat: 1st version of icons: champion, trash, yellow faction (so far)
release-tag / release-image (push) Has been cancelled
2026-03-15 16:23:18 +01:00
17 changed files with 257 additions and 55 deletions
+4
View File
@@ -162,3 +162,7 @@ cython_debug/
typings/*
output/*
.idea/*
src/convert/output/**/*
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -14,6 +14,7 @@ class Translation(TypedDict):
class SkinData(TypedDict):
title: str
flavor_text_type: str | None
flavor_text_bottom: str | None
image_path: str
+17
View File
@@ -0,0 +1,17 @@
# TODO next steps
local file used to track next stuff to do
- Add details on Damage
- Add SVG image for heal
- Add bleeding-safe deadzone around PNG output for printing
- Different icons for factions?
- Add shadow on some pictures that do not cut well (as a skin option, transparent gradient)
- Fix larger names
- Fix 20
- Texte du Con Artist avec la bonne couleur; Pareil pour Kikuri
## Idées
- 69: demonic cloud -> darkness CSM
View File
+23
View File
@@ -0,0 +1,23 @@
import os
gtkbin = r'C:\Program Files\GTK3-Runtime Win64\bin'
add_dll_dir = getattr(os, 'add_dll_directory', None)
if callable(add_dll_dir):
add_dll_dir(gtkbin)
else:
os.environ['PATH'] = os.pathsep.join((gtkbin, os.environ['PATH']))
import cairosvg
from pathlib import Path
folder = Path(r"C:\Users\Luka\Documents\dev\cards-gen\output\heron")
for svg_file in folder.glob("*.svg"):
print("Processing" + str(svg_file))
cairosvg.svg2png(
url=str(svg_file),
scale=3,
write_to=f'output/{str(svg_file.name).replace(".svg", ".png")}'
)
+19 -6
View File
@@ -13,12 +13,13 @@
},
"3": {
"title": "Commandement",
"flavor_text_bottom": "Haniwas, à l'attaque! Go!",
"image_path": "../../img/skins/heron/003.png"
},
"4": {
"title": "Saki, Pégase de Guerre",
"flavor_text_type": "Cowboy",
"flavor_text_bottom": "Howdy, cowboy!",
"image_path": "../../img/skins/heron/004.png"
},
"5": {
@@ -44,6 +45,7 @@
"9": {
"title": "Dlanor A. Knox",
"flavor_text_type": "Inquisitrice",
"flavor_text_bottom": "Dame Erika vous veut MORT.",
"image_path": "../../img/skins/heron/009.png"
},
"10": {
@@ -74,6 +76,7 @@
"15": {
"title": "Madeline, Mercenaire",
"flavor_text_type": "Guerrière",
"flavor_text_bottom": "Salut, c'est Madeline! Dites,\n vous n'auriez pas vu Mauvier?",
"image_path": "../../img/skins/heron/015.png"
},
"16": {
@@ -83,7 +86,7 @@
},
"17": {
"title": "Menace de Mort",
"flavor_text_type": "Takano",
"flavor_text_type": "Pas gentil",
"image_path": "../../img/skins/heron/017.png"
},
"18": {
@@ -94,7 +97,7 @@
"19": {
"title": "Niko Incendiaire",
"flavor_text_type": "gfhd;spfgljh.-",
"image_path": "../../img/skins/heron/019.gif"
"image_path": "../../img/skins/heron/019.png"
},
"20": {
"title": "Tit job",
@@ -123,7 +126,7 @@
},
"25": {
"title": "Jester, Maître Assassin",
"flavor_text_bottom": "Son nom maintient les employés\nde la compagnie éveillés.",
"image_path": "../../img/skins/heron/025.png"
},
"26": {
@@ -139,6 +142,7 @@
"28": {
"title": "Cirno",
"flavor_text_type": "Baka",
"flavor_text_bottom": "Dansu no chansu ni pankuna\nranbu de anta no mausu wa anguri\n nan chi ~yuu fanki!",
"image_path": "../../img/skins/heron/028.png"
},
"29": {
@@ -159,6 +163,7 @@
"32": {
"title": "Bracken",
"flavor_text_type": "Entité",
"flavor_text_bottom": "\"MAIS REND MON POTE\"",
"image_path": "../../img/skins/heron/032.png"
},
"33": {
@@ -253,7 +258,7 @@
},
"51": {
"title": "Baiken, Samurai Slasher",
"flavor_text_bottom": "Il n'y a pas de victoire ou défaite.\nJuste du savoir sur soi-même.",
"image_path": "../../img/skins/heron/051.png"
},
"52": {
@@ -296,14 +301,22 @@
"image_path": "../../img/skins/heron/059.png"
},
"60": {
"title": "Collection de Bounty",
"image_path": "../../img/skins/heron/060.png"
},
"61": {
"title": "Forethought",
"title": "Dangereuse échappée",
"image_path": "../../img/skins/heron/061.png"
},
"62": {
"title": "Festival de Watanagashi",
"image_path": "../../img/skins/heron/062.png"
},
"63": {
"title": "Plus la même",
"image_path": "../../img/skins/heron/063.png"
},
"65": {
"title": "Starlight Convergence",
"image_path": "../../img/skins/heron/065.png"
+2 -1
View File
@@ -39,7 +39,8 @@
"control_opposing_champion_passive":"Prenez le contrôle d'un Champion\nennemi jusqu'à la fin du tour.",
"buy_for_free_amount":"Vous pouvez acquérir gratuitement\nune carte de coût %a ou moins.",
"damage_all_champions_amount":"Infligez %a dégats à tous\nles Champions adverses.",
"discard_x_and_draw_x":"Vous pouvez vous défausser de\nn'importe quel nombre de cartes\net en piocher autant."
"discard_x_and_draw_x":"Vous pouvez vous défausser de\nn'importe quel nombre de cartes\net en piocher autant.",
"buy_gem_for_free": "Obtenez une gemme de coût 2\n ou moins gratuitement."
},
"per":{
"champion":"pour chaque Champion\nque vous avez en jeu.",
+123 -3
View File
@@ -1,10 +1,12 @@
from math import sin, cos, pi
from typing import Optional
import drawsvg as dw
import json
from itertools import groupby
from card_types import Card, Effect, Translation
from card_types import Card, Effect, Translation, SkinData
stats_radius = 20
effect_text_size = 14
@@ -26,6 +28,7 @@ def is_base_effect(effect: Effect):
def create_icon(
amount: int,
fill_circle: str,
outline_circle: Optional[str] = None,
effect_id: str | None = None,
fill_text: str = "black",
radius: float = stats_radius,
@@ -35,7 +38,9 @@ def create_icon(
else:
click_function = js_event_function(effect_id)
group = dw.Group(**{"class": "svg_resource_icon", "v-on:click": click_function})
group.append(dw.Circle(cx=0, cy=0, r=radius, fill=fill_circle, x=0, y=0))
if outline_circle:
group.append(dw.Circle(cx=0, cy=0, r=radius, fill=outline_circle, x=0, y=0))
group.append(dw.Circle(cx=0, cy=0, r=radius-2, fill=fill_circle, x=0, y=0))
group.append(
dw.Text(
str(amount),
@@ -58,6 +63,7 @@ def create_damage_icon(
amount,
radius=radius,
fill_circle="red",
outline_circle="black",
fill_text="white",
effect_id=effect_id,
)
@@ -70,6 +76,7 @@ def create_heal_icon(
amount,
radius=radius,
fill_circle="green",
outline_circle="black",
fill_text="white",
effect_id=effect_id,
)
@@ -82,6 +89,7 @@ def create_gold_icon(
amount,
radius=radius,
fill_circle="yellow",
outline_circle="goldenrod",
effect_id=effect_id,
)
@@ -313,6 +321,31 @@ def create_effect_text(
return group
def create_arrow():
return dw.Lines(7, 10,
7, -5,
-5, -5,
-5, -9,
-10, -2,
-5, 4,
-5, 0,
2, 0,
2, 10,
stroke='black', fill='white', close='true')
def create_faction_icon(faction: str, transform: str = None):
if faction == "yellow":
return dw.Lines(9, 0,
2, -2,
0, -5,
-2, -2,
-9, 0,
-2, 2,
0, 5,
2, 2,
stroke='grey', fill='white', close='true', transform=transform)
def create_effect_row(
card: Card, effects: list[Effect], type: str, height: int, width: int
@@ -330,6 +363,9 @@ def create_effect_row(
filter="url(#drop-shadow)",
)
)
subg = dw.Group(transform=f"translate({30},{height / 2})")
subg.append(create_faction_icon(faction="yellow"))
group.append(subg)
elif type == "suicide":
has_icon = True
group.append(
@@ -341,6 +377,30 @@ def create_effect_row(
filter="url(#drop-shadow)",
)
)
group.append(
dw.Line(
sx=19,
sy=(height / 2) - 11,
ex=41,
ey=(height / 2) + 11,
stroke="black",
stroke_width=2,
stroke_dasharray='3,1',
stroke_opacity=0.3,
)
)
group.append(
dw.Line(
sx=41,
sy=(height / 2) - 11,
ex=19,
ey=(height / 2) + 11,
stroke="black",
stroke_width=2,
stroke_dasharray='3,1',
stroke_opacity=0.3,
)
)
elif type == "champion_action":
has_icon = True
@@ -349,10 +409,24 @@ def create_effect_row(
cx=30,
cy=height / 2,
r=15,
fill="#444444",
filter="url(#drop-shadow)",
)
)
group.append(
dw.Circle(
cx=30,
cy=height / 2,
r=13,
fill="gray",
filter="url(#drop-shadow)",
)
)
subg = dw.Group(transform=f"translate({30},{height / 2})")
subg.append(create_arrow())
group.append(subg)
effect_with_icons = list(
filter(lambda x: x.get("effect") in resourceIcons and not x.get("per"), effects)
)
@@ -493,7 +567,7 @@ def generate_subeffect_index(effect: Effect):
generate_subeffect_index(e)
def create_effects(card: Card, effects: list[Effect], height: int, width: int):
def create_effects(card: Card, effects: list[Effect], height: int, width: int, flavor_text: Optional[str]):
for [i, e] in enumerate(effects):
e["index"] = str(i)
generate_subeffect_index(e)
@@ -509,6 +583,7 @@ def create_effects(card: Card, effects: list[Effect], height: int, width: int):
effect_types = list(grouped_effects.keys())
if len(grouped_effects) == 1:
if flavor_text is None:
group.append(
create_effect_row(
card=card,
@@ -518,6 +593,51 @@ def create_effects(card: Card, effects: list[Effect], height: int, width: int):
width=width,
),
)
else:
group.append(
dw.Line(sx=10, sy=90, ex=width - 10, ey=90, stroke="black", stroke_width=1)
)
group.append(
create_effect_row(
card=card,
effects=grouped_effects[effect_types[0]],
type=effect_types[0],
height=85,
width=width,
),
)
font_width = 0.5
line_width = (
max(*[len(x) for x in flavor_text.split("\n")], len(flavor_text)) * font_width
)
# Take width in account
font_size = effect_text_size - 1
font_size = min(
font_size, (font_size * 3) - line_width, font_size * width / (line_width or 1)
)
# Take height in account
font_size = min((height) / (flavor_text.count("\n") or 1), font_size)
font_size = max(font_size, 11)
group.append(
dw.Group(
[
dw.Text(
text=flavor_text, # TODO: mettre en italique & gris
x=0,
y=(-font_size / 2) * flavor_text.count("\n"),
text_anchor="middle",
fill="black",
font_size=font_size,
font_style="italic",
font_family="Helvetica",
transform=f"translate({width / 2},{height - 73})"
)
],
transform=f"translate({0},{55})"
)
)
if len(grouped_effects) == 2:
group.append(
dw.Line(sx=10, sy=80, ex=width - 10, ey=80, stroke="black", stroke_width=1)
+19 -5
View File
@@ -1,3 +1,5 @@
import base64
import drawsvg as dw
import json
import os
@@ -10,6 +12,7 @@ from card_types import Card, SkinData, SkinFile, Translation
from drawsvg.drawing import Drawing
from edit_card import finish_card
from src.draw_functions import create_faction_icon
with open("src/data/translation_fr.json", encoding="utf-8") as f:
translation_fr: Translation = json.load(f)
@@ -24,6 +27,8 @@ for p in Path("src/data/cards-json").glob("*.json"):
cards.extend(temp)
# Params
embedded_images = False
x_width = 298
y_height = 417
border_width_x = 12
@@ -74,7 +79,13 @@ for card_data in cards:
# Get card skin data
try:
card_skin_data = all_skin_data["cards"][str(card_data["id"])]
except KeyError:
if embedded_images:
with open(card_skin_data["image_path"].replace("../../", ""), "rb") as img:
b64 = base64.b64encode(img.read()).decode()
data_uri = f"data:image/png;base64,{b64}"
card_skin_data["image_path"] = data_uri
except KeyError as e:
print(
"Missing skin data for card id:", card_data["id"], "using default skin data"
)
@@ -84,9 +95,9 @@ for card_data in cards:
flavor_text_type=None,
)
# Check if card image really exists
if not Path(card_skin_data["image_path"].replace("../../", "")).absolute().exists():
print("Missing image for card id: ", card_data["id"], " , using default image")
card_skin_data["image_path"] = "../../img/missing.png"
# if not Path(card_skin_data["image_path"].replace("../../", "")).absolute().exists():
# print("Missing image for card id: ", card_data["id"], " , using default image")
# card_skin_data["image_path"] = "../../img/missing.png"
# Create a new SVG drawing
output_dir = "output/" + all_skin_data["skin_name"]
os.makedirs(output_dir, exist_ok=True)
@@ -113,6 +124,9 @@ for card_data in cards:
fill=f'url(#grad2-{card_data["id"]})',
)
)
faction_icon.append(
create_faction_icon("yellow", transform="scale(1.5)"),
)
# Define linear colored gradient
gradient = dw.LinearGradient(
@@ -257,7 +271,7 @@ for card_data in cards:
if effects is not None:
effects_height = 265 if card_data["id"] not in fullscreen_card_ids else 220
effects = create_effects(
card=card_data, effects=effects, height=135, width=x_internal_width
card=card_data, effects=effects, height=135, width=x_internal_width, flavor_text=card_skin_data["flavor_text_bottom"] if "flavor_text_bottom" in card_skin_data.keys() else None
)
d.append(
dw.Group(