Pixellab based system
This commit is contained in:
@@ -6,6 +6,9 @@ Pipeline: Jev picks a type -> seeded RNG picks a base creature and its variation
|
||||
(weighted by type) -> parts snap together via anchor pixels -> markings painted on ->
|
||||
palette keys swapped for colors pulled from the business logo.
|
||||
|
||||
Two part libraries share this pipeline (see LIBRARIES): "pixellab" (64x64, art generated with
|
||||
PixelLab, sprite_parts_pl.py) and "classic" (96x96, hand-written grids, sprite_parts.py).
|
||||
|
||||
Logo colors: transparent pixels, anti-aliased edges and a solid white/black/gray background
|
||||
(the colour filling the image border) are ignored; hued brand colours win over white, black
|
||||
or gray text. A logo that is only white/black (or only its background) still uses that.
|
||||
@@ -14,8 +17,9 @@ Usage
|
||||
python generate_sprite.py "Rosas Landscaping" logos/rosas.png # Jev picks the type
|
||||
python generate_sprite.py "Rosas Landscaping" logos/rosas.png --type grass
|
||||
python generate_sprite.py "Rosas Landscaping" --type grass --colors "#2E7D32,#FBC02D"
|
||||
python generate_sprite.py "Rosas Landscaping" logos/rosas.png --compare # both libraries side by side
|
||||
python generate_sprite.py --batch businesses.csv # columns: name,logo,type,description
|
||||
python generate_sprite.py --make-placeholders # test art for every part
|
||||
python generate_sprite.py --library classic --make-placeholders # test art for every part
|
||||
python generate_sprite.py --sprite-list SPRITES.md # art checklist + weight report
|
||||
|
||||
Requires: pip install pillow numpy requests
|
||||
@@ -24,6 +28,7 @@ import argparse
|
||||
import colorsys
|
||||
import csv
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
@@ -34,10 +39,20 @@ from pathlib import Path
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import sprite_parts as cfg
|
||||
from sprite_parts import TYPE_COLORS, TYPES
|
||||
|
||||
# Part libraries: registry module per library. Each defines NAME, CANVAS (final sprite size),
|
||||
# GROUND_MARGIN (empty rows under the feet), PARTS_DIR, and the picking tables (BODY, SLOTS,
|
||||
# PLAN_WEIGHTS, MARKINGS, MOTIFS, ...) documented in sprite_parts.py.
|
||||
LIBRARIES = {"pixellab": "sprite_parts_pl", "classic": "sprite_parts"}
|
||||
DEFAULT_LIBRARY = "pixellab"
|
||||
|
||||
|
||||
def load_library(name):
|
||||
if name not in LIBRARIES:
|
||||
sys.exit(f"Unknown library '{name}'. Options: {', '.join(LIBRARIES)}")
|
||||
return importlib.import_module(LIBRARIES[name])
|
||||
|
||||
CANVAS = 96 # final sprite size (square)
|
||||
GROUND_MARGIN = 4 # empty rows under the feet
|
||||
|
||||
# Palette keys: draw every part using ONLY these exact colors (plus pass-through colors
|
||||
# like eye white). They get swapped for logo-derived colors at render time.
|
||||
@@ -50,6 +65,18 @@ KEYS = {
|
||||
PUPIL = (20, 20, 20) # near-black that is NOT recolored
|
||||
EYE_WHITE = (255, 255, 255)
|
||||
|
||||
# Fixed-color ramps (light, base, dark) for elemental features that keep their own colors
|
||||
# whatever the brand: fire stays yellow/orange/red, ice stays icy. Like eye white they are
|
||||
# never recolored; parts that use them are type-locked in the library registry.
|
||||
FIXED = {
|
||||
"fire": ((255, 230, 107), (255, 159, 46), (226, 70, 43)),
|
||||
"bluefire": ((214, 246, 255), (92, 200, 255), (47, 107, 232)),
|
||||
"ice": ((242, 252, 255), (168, 230, 247), (90, 169, 214)),
|
||||
"electric": ((255, 246, 168), (255, 217, 59), (224, 162, 26)),
|
||||
"ghostfire": ((240, 210, 255), (185, 123, 240), (116, 69, 184)),
|
||||
}
|
||||
FIXED_COLORS = {c for ramp in FIXED.values() for c in ramp}
|
||||
|
||||
# Anchor marker pixels. Place one pixel of the color where a child part attaches.
|
||||
# They are read, then replaced with the surrounding color.
|
||||
MARKERS = {
|
||||
@@ -90,9 +117,9 @@ def part_weight(p, ptype):
|
||||
return p["types"].get(ptype, p["other"])
|
||||
|
||||
|
||||
def candidates(slot, plan, ptype, tags, excluded, denied=()):
|
||||
def candidates(lib, slot, plan, ptype, tags, excluded, denied=()):
|
||||
out = []
|
||||
for p in cfg.SLOTS[slot]:
|
||||
for p in lib.SLOTS[slot]:
|
||||
if plan not in p["plans"] or p["name"] in denied:
|
||||
continue
|
||||
if p["requires"] and not set(p["requires"]) & tags:
|
||||
@@ -103,30 +130,37 @@ def candidates(slot, plan, ptype, tags, excluded, denied=()):
|
||||
return out
|
||||
|
||||
|
||||
def motif_factor(p, motifs):
|
||||
def motif_factor(lib, p, motifs):
|
||||
"""Keep one creature's features on a theme: same motif boosted, clashing motifs damped."""
|
||||
m = cfg.MOTIFS.get(p["name"])
|
||||
m = lib.MOTIFS.get(p["name"])
|
||||
if m is None or not motifs:
|
||||
return 1.0
|
||||
if m in motifs:
|
||||
return cfg.MOTIF_SAME
|
||||
if all(frozenset((m, o)) in cfg.MOTIF_PAIRS for o in motifs):
|
||||
return lib.MOTIF_SAME
|
||||
if all(frozenset((m, o)) in lib.MOTIF_PAIRS for o in motifs):
|
||||
return 1.0
|
||||
return cfg.MOTIF_CLASH
|
||||
return lib.MOTIF_CLASH
|
||||
|
||||
|
||||
def pick_parts(ptype, rng):
|
||||
plan = weighted_pick(rng, cfg.PLAN_WEIGHTS[ptype].items())
|
||||
def pick_parts(lib, ptype, rng):
|
||||
"""Seeded pick of body plan, body, feature parts and markings. A library may define
|
||||
fits(body, picks) to drop candidates that would push the composite past its canvas."""
|
||||
plan = weighted_pick(rng, lib.PLAN_WEIGHTS[ptype].items())
|
||||
chosen, tags, excluded, denied, motifs = {}, {f"plan:{plan}"}, set(), set(), set()
|
||||
features = 0
|
||||
fits = getattr(lib, "fits", None)
|
||||
for slot in PICK_ORDER:
|
||||
if slot in cfg.DISABLED_SLOTS:
|
||||
if slot in lib.DISABLED_SLOTS:
|
||||
continue
|
||||
cands = candidates(slot, plan, ptype, tags, excluded, denied)
|
||||
if slot in cfg.FEATURE_SLOTS:
|
||||
if features >= cfg.MAX_FEATURES:
|
||||
cands = candidates(lib, slot, plan, ptype, tags, excluded, denied)
|
||||
if slot in lib.FEATURE_SLOTS:
|
||||
if features >= lib.MAX_FEATURES:
|
||||
cands = [(p, w) for p, w in cands if p["name"] == "none"]
|
||||
cands = [(p, w * motif_factor(p, motifs)) for p, w in cands]
|
||||
cands = [(p, w * motif_factor(lib, p, motifs)) for p, w in cands]
|
||||
if fits:
|
||||
picked = {s: q for s, q in chosen.items() if s != "body"}
|
||||
cands = [(p, w) for p, w in cands
|
||||
if p["name"] == "none" or fits(chosen["body"], {**picked, slot: p})]
|
||||
p = weighted_pick(rng, cands)
|
||||
if p is None:
|
||||
continue
|
||||
@@ -134,11 +168,11 @@ def pick_parts(ptype, rng):
|
||||
tags |= set(p["tags"])
|
||||
excluded |= set(p["excludes"])
|
||||
denied |= set(p.get("deny", ()))
|
||||
if p["name"] != "none" and slot in cfg.FEATURE_SLOTS:
|
||||
if p["name"] != "none" and slot in lib.FEATURE_SLOTS:
|
||||
features += 1
|
||||
if cfg.MOTIFS.get(p["name"]):
|
||||
motifs.add(cfg.MOTIFS[p["name"]])
|
||||
markings = weighted_pick(rng, [(m, w.get(ptype, w.get("_other", 0))) for m, w in cfg.MARKINGS.items()])
|
||||
if lib.MOTIFS.get(p["name"]):
|
||||
motifs.add(lib.MOTIFS[p["name"]])
|
||||
markings = weighted_pick(rng, [(m, w.get(ptype, w.get("_other", 0))) for m, w in lib.MARKINGS.items()])
|
||||
return plan, chosen, markings, tags
|
||||
|
||||
|
||||
@@ -281,13 +315,13 @@ def compose(parts_dir, chosen, markings, rng):
|
||||
return canvas.crop(canvas.getbbox())
|
||||
|
||||
|
||||
def place_on_canvas(sprite):
|
||||
def place_on_canvas(sprite, lib):
|
||||
w, h = sprite.size
|
||||
size = max(CANVAS, w + 2, h + GROUND_MARGIN + 1)
|
||||
if size > CANVAS:
|
||||
size = max(lib.CANVAS, w + 2, h + lib.GROUND_MARGIN + 1)
|
||||
if size > lib.CANVAS:
|
||||
warn(f"sprite is {w}x{h}, canvas expanded to {size}px")
|
||||
out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
out.alpha_composite(sprite, ((size - w) // 2, size - GROUND_MARGIN - h))
|
||||
out.alpha_composite(sprite, ((size - w) // 2, size - lib.GROUND_MARGIN - h))
|
||||
return out
|
||||
|
||||
|
||||
@@ -445,7 +479,7 @@ def extract_logo_colors(path, n=6):
|
||||
|
||||
|
||||
def pick_main_accent(colors, ptype):
|
||||
type_rgb = hex_to_rgb(cfg.TYPE_COLORS[ptype])
|
||||
type_rgb = hex_to_rgb(TYPE_COLORS[ptype])
|
||||
if not colors:
|
||||
main = type_rgb
|
||||
accent = rotate_hue(type_rgb, 0.5)
|
||||
@@ -469,7 +503,7 @@ def pick_main_accent(colors, ptype):
|
||||
|
||||
|
||||
def build_palette(main, accent, ptype):
|
||||
type_rgb = hex_to_rgb(cfg.TYPE_COLORS[ptype])
|
||||
type_rgb = hex_to_rgb(TYPE_COLORS[ptype])
|
||||
# Elemental parts (leaves, flames, sparks) must read against the body: if the type colour
|
||||
# is close to the main colour, push its lightness away from the main colour.
|
||||
if color_dist(type_rgb, main) < 150:
|
||||
@@ -499,17 +533,23 @@ def classify(description):
|
||||
return ans["choice"].lower(), ans.get("probabilities")
|
||||
|
||||
|
||||
def generate(name, parts_dir, out_dir, ptype=None, description=None, logo=None, colors=None,
|
||||
reroll=0, scale=4):
|
||||
def resolve_type(name, ptype=None, description=None):
|
||||
"""(type, probabilities). Jev classifies when no type is given."""
|
||||
probs = None
|
||||
if not ptype:
|
||||
ptype, probs = classify(description or name)
|
||||
ptype = ptype.lower()
|
||||
if ptype not in cfg.PLAN_WEIGHTS:
|
||||
sys.exit(f"Unknown type '{ptype}'. Options: {', '.join(cfg.TYPES)}")
|
||||
if ptype not in TYPES:
|
||||
sys.exit(f"Unknown type '{ptype}'. Options: {', '.join(TYPES)}")
|
||||
return ptype, probs
|
||||
|
||||
|
||||
def generate(lib, name, out_dir, ptype, probs=None, logo=None, colors=None, reroll=0, scale=4,
|
||||
parts_dir=None, suffix=""):
|
||||
"""Render one sprite with library `lib`; writes <slug><suffix>.png/.json (+ upscaled copy).
|
||||
Returns (info, sprite)."""
|
||||
rng = make_rng(name, reroll)
|
||||
plan, chosen, markings, tags = pick_parts(ptype, rng)
|
||||
plan, chosen, markings, tags = pick_parts(lib, ptype, rng)
|
||||
|
||||
if colors:
|
||||
logo_colors = [(hex_to_rgb(c), 1.0) for c in colors]
|
||||
@@ -522,26 +562,53 @@ def generate(name, parts_dir, out_dir, ptype=None, description=None, logo=None,
|
||||
main, accent = pick_main_accent(logo_colors, ptype)
|
||||
palette = build_palette(main, accent, ptype)
|
||||
|
||||
sprite = recolor(place_on_canvas(compose(parts_dir, chosen, markings, rng)), palette)
|
||||
sprite = recolor(place_on_canvas(compose(parts_dir or lib.PARTS_DIR, chosen, markings, rng), lib), palette)
|
||||
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
slug = slugify(name) + (f"-r{reroll}" if reroll else "")
|
||||
slug = slugify(name) + (f"-r{reroll}" if reroll else "") + suffix
|
||||
sprite.save(out_dir / f"{slug}.png")
|
||||
if scale > 1:
|
||||
sprite.resize((sprite.width * scale, sprite.height * scale), Image.NEAREST).save(out_dir / f"{slug}@{scale}x.png")
|
||||
|
||||
info = {
|
||||
"name": name, "type": ptype, "type_probabilities": probs, "plan": plan,
|
||||
"name": name, "library": lib.NAME, "type": ptype, "type_probabilities": probs, "plan": plan,
|
||||
"parts": {s: p["name"] for s, p in chosen.items()}, "markings": markings,
|
||||
"attributes": attributes_from(tags),
|
||||
"colors": {"main": rgb_to_hex(main), "accent": rgb_to_hex(accent),
|
||||
"type": cfg.TYPE_COLORS[ptype], "outline": rgb_to_hex(palette["outline"])},
|
||||
"type": TYPE_COLORS[ptype], "outline": rgb_to_hex(palette["outline"])},
|
||||
}
|
||||
(out_dir / f"{slug}.json").write_text(json.dumps(info, indent=2))
|
||||
print(f"{name}: {ptype.upper()} {plan} | {', '.join(info['attributes']) or 'no attributes'} | "
|
||||
print(f"{name} [{lib.NAME}]: {ptype.upper()} {plan} | {', '.join(info['attributes']) or 'no attributes'} | "
|
||||
f"main {info['colors']['main']} accent {info['colors']['accent']} -> {out_dir / slug}.png")
|
||||
return info
|
||||
return info, sprite
|
||||
|
||||
|
||||
COMPARE_SIZE = 384 # each sprite is scaled by a whole factor to this height in the comparison
|
||||
|
||||
|
||||
def write_compare(path, title, rendered):
|
||||
"""Side-by-side sheet. rendered: [(info, sprite)], one per library."""
|
||||
pad, label = 16, 28
|
||||
cells = []
|
||||
for info, spr in rendered:
|
||||
k = max(1, COMPARE_SIZE // spr.height)
|
||||
big = spr.resize((spr.width * k, spr.height * k), Image.NEAREST)
|
||||
cells.append((f"{info['library']} ({spr.width}x{spr.height}): {info['plan']}, "
|
||||
f"{', '.join(n for s, n in info['parts'].items() if s != 'body' and n != 'none') or 'no extras'}",
|
||||
big))
|
||||
w = sum(c.width for _, c in cells) + pad * (len(cells) + 1)
|
||||
h = max(c.height for _, c in cells) + pad * 2 + label * 2
|
||||
sheet = Image.new("RGBA", (w, h), (232, 232, 226, 255))
|
||||
d = ImageDraw.Draw(sheet)
|
||||
d.text((pad, pad // 2), title, fill=(30, 30, 30, 255))
|
||||
x = pad
|
||||
for text, big in cells:
|
||||
d.text((x, pad // 2 + label), text, fill=(60, 60, 60, 255))
|
||||
sheet.alpha_composite(big, (x, pad + label * 2))
|
||||
x += big.width + pad
|
||||
sheet.save(path)
|
||||
print(f"comparison -> {path}")
|
||||
|
||||
|
||||
# ============================================================ placeholders
|
||||
@@ -603,9 +670,9 @@ def draw_placeholder(slot, p):
|
||||
return img
|
||||
|
||||
|
||||
def make_placeholders(parts_dir, overwrite=False):
|
||||
def make_placeholders(lib, parts_dir, overwrite=False):
|
||||
made = 0
|
||||
for slot, parts in cfg.SLOTS.items():
|
||||
for slot, parts in lib.SLOTS.items():
|
||||
(Path(parts_dir) / slot).mkdir(parents=True, exist_ok=True)
|
||||
for p in parts:
|
||||
if p["name"] == "none":
|
||||
@@ -620,10 +687,10 @@ def make_placeholders(parts_dir, overwrite=False):
|
||||
|
||||
# ============================================================ sprite list + weight report
|
||||
|
||||
def simulate(ptype, n=3000):
|
||||
def simulate(lib, ptype, n=3000):
|
||||
plans, slots, marks = Counter(), {s: Counter() for s in PICK_ORDER}, Counter()
|
||||
for i in range(n):
|
||||
plan, chosen, m, _ = pick_parts(ptype, random.Random(i))
|
||||
plan, chosen, m, _ = pick_parts(lib, ptype, random.Random(i))
|
||||
plans[plan] += 1
|
||||
marks[m] += 1
|
||||
for s in PICK_ORDER:
|
||||
@@ -635,20 +702,23 @@ def pct(counter, n, top=None):
|
||||
return ", ".join(f"{k} {v / n:.0%}" for k, v in counter.most_common(top) if v / n >= 0.01)
|
||||
|
||||
|
||||
def write_sprite_list(path):
|
||||
L = ["# Sprite checklist", ""]
|
||||
total = sum(1 for parts in cfg.SLOTS.values() for p in parts if p["name"] != "none")
|
||||
L += [f"**{total} sprites** across {len(cfg.SLOTS)} slots. Save each as `parts/<slot>/<name>.png`.", "",
|
||||
def write_sprite_list(lib, path):
|
||||
L = [f"# Sprite checklist ({lib.NAME} library)", ""]
|
||||
total = sum(1 for parts in lib.SLOTS.values() for p in parts if p["name"] != "none")
|
||||
L += [f"**{total} sprites** across {len(lib.SLOTS)} slots. Save each as `{lib.PARTS_DIR}/<slot>/<name>.png`.", "",
|
||||
"## Art rules", "",
|
||||
f"- Final canvas is {CANVAS}x{CANVAS}. Sizes below are suggested part canvases; crop tight with transparency.",
|
||||
f"- Final canvas is {lib.CANVAS}x{lib.CANVAS}. Sizes below are suggested part canvases; crop tight with transparency.",
|
||||
"- Characters face left in a 3/4 view, light from the top-left. 1px outline, no anti-aliasing, no semi-transparent pixels.",
|
||||
"- Draw only with the palette keys below. Any other color (eye white, pupils) is left as-is.", ""]
|
||||
"- Draw only with the palette keys below. Any other color (eye white, pupils, fixed ramps) is left as-is.", ""]
|
||||
L += ["| Key | Hex | Use |", "|---|---|---|"]
|
||||
uses = {"main": "body / most fur and skin", "accent": "horns, wings, beaks, crests, shells",
|
||||
"type": "elemental bits: flames, leaves, sparks, ice, water"}
|
||||
for k, v in KEYS.items():
|
||||
L.append(f"| {k} | {rgb_to_hex(v)} | {'outline (recolored)' if k == 'outline' else uses[k.split('_')[0]]} |")
|
||||
L += [f"| pupil | {rgb_to_hex(PUPIL)} | not recolored |", f"| eye white | {rgb_to_hex(EYE_WHITE)} | not recolored |", ""]
|
||||
L += [f"| pupil | {rgb_to_hex(PUPIL)} | not recolored |", f"| eye white | {rgb_to_hex(EYE_WHITE)} | not recolored |"]
|
||||
for name, shades in FIXED.items():
|
||||
L.append(f"| {name} (light, base, dark) | {', '.join(rgb_to_hex(c) for c in shades)} | fixed color, not recolored |")
|
||||
L.append("")
|
||||
L += ["### Anchor marker pixels", "",
|
||||
"One pixel each. `pivot` is where the part attaches to its parent; the others are where children attach.", "",
|
||||
"| Marker | Hex | Needed in |", "|---|---|---|",
|
||||
@@ -663,18 +733,18 @@ def write_sprite_list(path):
|
||||
L += [f"## {slot} ({len(parts)})", "", "| Done | File | Size | Palette | Plans | Description | Most likely for |",
|
||||
"|---|---|---|---|---|---|---|"]
|
||||
for p in parts:
|
||||
top = sorted(cfg.TYPES, key=lambda t: -part_weight(p, t))[:3]
|
||||
top = sorted(TYPES, key=lambda t: -part_weight(p, t))[:3]
|
||||
top = ", ".join(t for t in top if part_weight(p, t) > 0)
|
||||
plans = "all" if p["plans"] == cfg.ALL else ", ".join(p["plans"])
|
||||
plans = "all" if set(p["plans"]) == set(lib.ALL) else ", ".join(p["plans"])
|
||||
w, h = size_for(slot, p)
|
||||
L.append(f"| [ ] | `{slot}/{p['name']}.png` | {w}x{h} | {p['palette']} | {plans} | {p['desc']} | {top} |")
|
||||
L.append("")
|
||||
|
||||
L += ["## Simulated pick rates per type", "",
|
||||
"From 3,000 random seeds per type. Use this to spot parts that never show up or show up too much.", ""]
|
||||
for t in cfg.TYPES:
|
||||
for t in TYPES:
|
||||
n = 3000
|
||||
plans, slots, marks = simulate(t, n)
|
||||
plans, slots, marks = simulate(lib, t, n)
|
||||
L += [f"### {t.capitalize()}", "", f"- **Body plan:** {pct(plans, n)}"]
|
||||
for s in PICK_ORDER:
|
||||
L.append(f"- **{s}:** {pct(slots[s], n, 8)}")
|
||||
@@ -685,6 +755,18 @@ def write_sprite_list(path):
|
||||
|
||||
# ============================================================ CLI
|
||||
|
||||
def run_one(a, name, ptype=None, description=None, logo=None, colors=None, reroll=0):
|
||||
"""Render `name` with --library, or with every library plus a comparison sheet (--compare)."""
|
||||
ptype, probs = resolve_type(name, ptype, description)
|
||||
if not a.compare:
|
||||
generate(load_library(a.library), name, a.out, ptype, probs, logo, colors, reroll, a.scale, a.parts)
|
||||
return
|
||||
rendered = [generate(load_library(n), name, a.out, ptype, probs, logo, colors, reroll, a.scale, suffix=f"-{n}")
|
||||
for n in LIBRARIES]
|
||||
slug = slugify(name) + (f"-r{reroll}" if reroll else "")
|
||||
write_compare(Path(a.out) / f"{slug}-compare.png", f"{name} | type: {ptype}", rendered)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Generate Pokémon-style business sprites")
|
||||
ap.add_argument("name", nargs="?", help="Business name (also the random seed)")
|
||||
@@ -694,7 +776,11 @@ def main():
|
||||
ap.add_argument("--logo", help="Logo image to pull colors from")
|
||||
ap.add_argument("--colors", help='Manual colors, e.g. "#2E7D32,#FBC02D" (main first)')
|
||||
ap.add_argument("--reroll", type=int, default=0, help="Alternate creature for the same business")
|
||||
ap.add_argument("--parts", default="parts", help="Parts folder")
|
||||
ap.add_argument("--library", choices=list(LIBRARIES), default=DEFAULT_LIBRARY,
|
||||
help=f"Part library (default {DEFAULT_LIBRARY})")
|
||||
ap.add_argument("--compare", action="store_true",
|
||||
help="Render with every library: <slug>-<library>.png plus <slug>-compare.png side by side")
|
||||
ap.add_argument("--parts", help="Parts folder (default: the library's own)")
|
||||
ap.add_argument("--out", default="sprites", help="Output folder")
|
||||
ap.add_argument("--scale", type=int, default=4, help="Also save an upscaled copy (1 to disable)")
|
||||
ap.add_argument("--batch", help="CSV with columns name,logo,type,description (only name required)")
|
||||
@@ -702,25 +788,27 @@ def main():
|
||||
ap.add_argument("--overwrite", action="store_true", help="With --make-placeholders, replace existing files")
|
||||
ap.add_argument("--sprite-list", metavar="PATH", help="Write the art checklist and weight report")
|
||||
a = ap.parse_args()
|
||||
if a.compare and a.parts:
|
||||
ap.error("--parts picks one library's folder; it can't be combined with --compare")
|
||||
|
||||
lib = load_library(a.library)
|
||||
if a.make_placeholders:
|
||||
make_placeholders(a.parts, a.overwrite)
|
||||
make_placeholders(lib, a.parts or lib.PARTS_DIR, a.overwrite)
|
||||
if a.sprite_list:
|
||||
write_sprite_list(a.sprite_list)
|
||||
write_sprite_list(lib, a.sprite_list)
|
||||
colors = a.colors.split(",") if a.colors else None
|
||||
if a.batch:
|
||||
with open(a.batch, newline="", encoding="utf-8") as f:
|
||||
for row in csv.DictReader(f):
|
||||
if not (row.get("name") or "").strip():
|
||||
continue
|
||||
generate(row["name"].strip(), a.parts, a.out, ptype=(row.get("type") or "").strip() or None,
|
||||
description=(row.get("description") or "").strip() or None,
|
||||
logo=(row.get("logo") or "").strip() or None, scale=a.scale)
|
||||
run_one(a, row["name"].strip(), (row.get("type") or "").strip() or None,
|
||||
(row.get("description") or "").strip() or None, (row.get("logo") or "").strip() or None)
|
||||
elif a.name:
|
||||
logo = a.logo or a.logo_file
|
||||
if logo and not Path(logo).exists():
|
||||
sys.exit(f"Logo not found: {logo}")
|
||||
generate(a.name, a.parts, a.out, a.type, a.desc, logo, colors, a.reroll, a.scale)
|
||||
run_one(a, a.name, a.type, a.desc, logo, colors, a.reroll)
|
||||
elif not (a.make_placeholders or a.sprite_list):
|
||||
ap.print_help()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user