""" 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 [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()