first commit yay

This commit is contained in:
2026-07-19 15:10:13 +03:00
parent 09e76f1135
commit 93dff7680d
253 changed files with 1765 additions and 2 deletions

304
main.py Normal file
View File

@@ -0,0 +1,304 @@
"""
Inscryption Card Generator
A command-line tool for generating custom Inscryption game cards.
Supports customization of card backgrounds, portraits, symbols, abilities, tribes,
costs, and other visual elements.
Authors: Kolyah35, FullHarmony
License: MIT
Usage:
python main.py <filename> [options]
Examples:
# Generate a single card
python main.py grizzly.png -p grizzly -ca 3 -ct blood -r 2 -n Grizzly -hp 6 -d 4
# Generate a card set from JSON
python main.py cardset_output_dir -cs cardset.json
# Generate with custom background and symbols
python main.py card.png -b rare -s ability_sharp ability_costly -t bird
Dependencies:
- PIL (Pillow)
"""
from PIL import Image, ImageFont, ImageDraw
import argparse
import os
import json
FONT_HEAVY_PATH = './assets/HEAVYWEIGHT.ttf'
def load_images_with_prefix(folder, prefix):
images = {}
for filename in os.listdir(folder):
if not filename.startswith(prefix):
continue
key = filename[len(prefix):]
key = os.path.splitext(key)[0]
images[key] = os.path.join(folder, filename)
return images
background_dict = load_images_with_prefix('./assets/backgrounds', '')
symbol_dict = load_images_with_prefix('./assets/symbols', 'ability_')
portrait_dict = load_images_with_prefix('./assets/portraits', 'portrait_')
deathcard_dict = load_images_with_prefix('./assets/deathcard', 'deathcard_')
decals_dict = load_images_with_prefix('./assets/decals', 'decal_')
tribes_dict = load_images_with_prefix('./assets/tribes', 'tribeicon_')
stats_dict = load_images_with_prefix('./assets/stats', 'stat_icon_')
def damage_or_staticon(value) -> int|str:
try:
return int(value)
except ValueError:
if value in stats_dict.keys():
return value
raise argparse.ArgumentTypeError(
f"Expected hp number or one of {stats_dict.keys()}"
)
parser = argparse.ArgumentParser(prog='Inscryption Card Generator')
parser.add_argument('filename', help='Path to a directory to generate a card set, or a file name to generate a single card')
parser.add_argument('-cs', '--cardset', type=str, help='Path to a card set')
parser.add_argument('-r', '--scale', type=int, default=2, help='Scale multiplier for card render')
parser.add_argument('-b', '--background', choices=background_dict.keys(), type=str, default='common', help='Card background type')
parser.add_argument('-s', '--symbols', type=str, nargs='+', choices=symbol_dict.keys(), help='Card symbols (max 4)')
parser.add_argument('-p', '--portrait', type=str, choices=portrait_dict.keys(), help='Card portrait type')
parser.add_argument('-dec', '--decal', type=str, choices=decals_dict.keys(), help='Card decal type')
parser.add_argument('-t', '--tribe', type=str, choices=list(tribes_dict.keys()).append('all'), help='Card tribe type')
parser.add_argument('-ca', '--cost-amount', type=int)
parser.add_argument('-ct', '--cost-type', type=str, choices=['blood', 'bone'])
parser.add_argument('-n', '--name', type=str, help='Card name')
parser.add_argument('-hp', '--hp', type=int, default=0)
parser.add_argument('-d', '--damage', type=damage_or_staticon, default=0)
parser.add_argument('--show', action='store_true', help='Show result in system image viewer')
args = parser.parse_args()
scale = args.scale
def img_scale(img, factor = scale):
return img.resize(
(int(img.width * factor), int(img.height * factor)),
Image.Resampling.NEAREST
)
def img_paste(img, dest, pos):
"""Paste image with position scaling"""
black = Image.new('RGBA', img.size, (0, 0, 0, 255))
dest.paste(black, (pos[0] * scale, pos[1] * scale), img)
def img_fit_text(img, text, font_path, box, max_size=200, min_size=8, align='center'):
draw = ImageDraw.Draw(img)
x, y, w, h = box
best_font = None
best_bbox = None
for size in range(max_size, min_size - 1, -1):
font = ImageFont.truetype(font_path, size)
bbox = draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
if tw <= w and th <= h:
best_font = font
best_bbox = (tw, th)
break
if best_font is None:
return False
if align == 'left':
tx = x
elif align == 'right':
tx = x + w - best_bbox[0]
else:
tx = x + (w - best_bbox[0]) // 2
ty = y + (h - best_bbox[1]) // 2
draw.text((tx, ty), text, font=best_font, fill=(0, 0, 0, 255))
return True
repeat_names = {}
# Generating image
def generate_card(
background: str,
name: str,
portrait: str,
symbols: list,
cost_type: str,
cost_amount: int,
hp: int,
damage: int|str,
decal: str,
tribe: str):
ca = cost_amount
ct = cost_type
if ca > 0 and not ct: ct = 'blood'
if (ct == 'blood' and ca > 4) or (ct == 'bone' and ca > 10):
print(f'Error: Cost "{ct}" can not be more than {4 if ct == "blood" else 10}!')
exit()
workspace = Image.open(f'./assets/backgrounds/{background}.png').convert('RGB')
workspace = workspace.resize((workspace.width * scale, workspace.height * scale), resample=Image.Resampling.NEAREST)
print(f'Generating card {name} - {workspace.width}x{workspace.height}...')
s_count = len(symbols) if symbols else 0
for i in range(s_count):
s = symbols[i]
s_variants = [
[(49, 144)],
[(49, 168), (74, 143)],
[(49, 143), (74, 143), (61, 168)],
[(49, 143), (74, 143), (49, 168), (74, 168)],
]
sl = Image.open(symbol_dict[s]).convert('RGBA')
s_scale = (1 if s_count == 1 else 0.5)
sl = img_scale(sl, scale * s_scale)
pos = s_variants[s_count-1][i]
img_paste(sl, workspace, pos)
if portrait:
if type(portrait) is str:
p = Image.open(portrait_dict[portrait]).convert('RGBA')
p = img_scale(p)
img_paste(p, workspace, (17, 44))
elif type(portrait) is dict:
for part in ('base', 'eyes', 'mouth'):
p = Image.open(deathcard_dict[f'{part}_{portrait[part]}']).convert('RGBA')
p = img_scale(p)
img_paste(p, workspace, (17, 44))
if tribe:
tribe_pos = {
'bird': (16, 18),
'canine': (90, 18),
'hooved': (164, 18),
'insect': (53, 167),
'reptile': (136, 167)
} if tribe == 'all' else {'_': (16, 18)}
tribes_to_render = tribe_pos.items() if tribe == 'all' else [(tribe, tribe_pos['_'])]
for tribe_name, pos in tribes_to_render:
tribe_key = tribe_name if tribe == 'all' else tribe
tribe_img = Image.open(f'./assets/tribes/tribeicon_{tribe_key}.png').convert('RGBA')
tribe_img = img_scale(tribe_img, scale / 2)
r, g, b, a = tribe_img.split()
a = a.point(lambda x: x * 0.5)
tribe_img = Image.merge('RGBA', (r, g, b, a))
workspace.paste(tribe_img, (pos[0] * int(scale / 2), pos[1] * int(scale / 2)), tribe_img)
if cost_type or cost_amount > 0:
cost_img = Image.open(f'./assets/costs/cost_{cost_amount}{cost_type}.png').convert('RGBA')
cost_img = img_scale(cost_img, scale)
workspace.paste(cost_img, (67 * scale, 27 * scale), cost_img)
if name.startswith('$'):
p = Image.open(f'./assets/{name[1:]}.png').convert('RGBA')
p = img_scale(p)
img_paste(p, workspace, (17, 16))
name = portrait
else:
img_fit_text(workspace, name, FONT_HEAVY_PATH, (20 * scale, 20 * scale, 107 * scale, 17 * scale))
if damage:
if type(damage) is int:
img_fit_text(workspace, str(damage), FONT_HEAVY_PATH, (23 * scale, 144 * scale, 20 * scale, 30 * scale))
if type(damage) is str:
stat_img = Image.open(f'./assets/stats/stat_icon_{damage}.png').convert('RGBA')
stat_img = img_scale(stat_img, scale)
img_paste(stat_img, workspace, (11, 138))
if hp:
img_fit_text(workspace, str(hp), FONT_HEAVY_PATH, (110 * scale, 158 * scale, 20 * scale, 30 * scale))
if decal:
decal_img = Image.open(f'./assets/decals/decal_{decal}.png').convert('RGBA')
decal_img = img_scale(decal_img)
workspace.paste(decal_img, (11 * scale, 11 * scale), decal_img)
if name not in repeat_names:
repeat_names[name] = 0
else:
repeat_names[name] += 1
if repeat_names[name] > 0:
name = f'{name}-{repeat_names[name]}'
return workspace, name
if args.cardset:
folder = os.path.join('.', args.filename)
if not os.path.exists(folder):
os.mkdir(folder)
cardset = json.load(open(args.cardset, encoding="utf-8"))
for card in cardset:
defaults = {
'background': 'common',
'name': '',
'portrait': '',
'symbols': [],
'cost-type': '',
'cost-amount': 0,
'hp': 0,
'damage': 0,
'decal': '',
'tribe': ''
}
for opt in defaults.items():
if opt[0] not in card:
card[opt[0]] = opt[1]
gen_card, name = generate_card(
background=card['background'],
name=card['name'],
portrait=card['portrait'],
symbols=card['symbols'],
cost_type=card['cost-type'],
cost_amount=card['cost-amount'],
hp=card['hp'],
damage=card['damage'],
decal=card['decal'],
tribe=card['tribe']
)
gen_card.save(os.path.join(folder, name + '.png'))
else:
gen_card, name = generate_card(
background=args.background,
name=args.name,
portrait=args.portrait,
symbols=args.symbols,
cost_type=args.cost_type,
cost_amount=args.cost_amount,
hp=args.hp,
damage=args.damage,
decal=args.decal,
tribe=args.tribe
)
gen_card.save(args.filename)
if args.show:
gen_card.show()