Pixellab library expansion
This commit is contained in:
@@ -8,17 +8,19 @@ specs into the picking tables generate_sprite.py uses. Only parts that have been
|
||||
(parts_pl/<slot>/<name>.png exists) are registered.
|
||||
|
||||
Differences from the classic library:
|
||||
- Every body is available to every type (`other` defaults to 1); `types` only boosts.
|
||||
- Every body is available to every type, but strongly prefers its own: a body's weight for a
|
||||
type is its `types` boost, or BODY_OTHER for types it doesn't list. Variants (edits of a
|
||||
body: same idea, different look) are bodies too and share their parent's plan and tags.
|
||||
- Parts with a fixed-color ramp ("fixed": "fire", ...) are type-locked: they must list
|
||||
`types` and have other=0.
|
||||
- `fits()` keeps every random pick inside the 64x64 canvas, so parts from any body can be
|
||||
offered to every body without hand-written size deny lists.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from sprite_parts import MOTIF_PAIRS as CLASSIC_MOTIF_PAIRS
|
||||
from sprite_parts import PLAN_WEIGHTS as CLASSIC_PLAN_WEIGHTS
|
||||
from sprite_parts import PLANS, TYPES, TYPE_COLORS, none, part # noqa: F401 (TYPES/TYPE_COLORS re-exported)
|
||||
|
||||
NAME = "pixellab"
|
||||
@@ -35,10 +37,10 @@ MOTIF_SAME, MOTIF_CLASH = 2.5, 0.08
|
||||
# Detailed generated art doesn't take painted-on spots well; markings stay off.
|
||||
MARKINGS = {"none": {"_other": 1}}
|
||||
# Weight of an empty slot, per slot. With MAX_FEATURES = 2 and head_top/back picked before tail,
|
||||
# these give about: head_top 69%, back 55%, tail 51% filled, 2% plain (simulated over all types
|
||||
# with 31 bodies / 111 parts). Re-tune when the library grows a lot.
|
||||
NONE_WEIGHT = {"head_top": 10.0, "back": 16.0, "tail": 1.0}
|
||||
PLAN_FLOOR = 1.0 # every plan stays possible for every type
|
||||
# these give about: head_top 59%, back 60%, tail 51% filled, 3% plain (simulated over all types
|
||||
# with 1208 bodies / 249 parts). Re-tune when the parts library grows a lot.
|
||||
NONE_WEIGHT = {"head_top": 20.0, "back": 28.0, "tail": 1.5}
|
||||
BODY_OTHER = 0.15 # a body's weight for types it doesn't list in `types` (listed types: 2-6)
|
||||
|
||||
|
||||
class SpecError(Exception):
|
||||
@@ -46,9 +48,17 @@ class SpecError(Exception):
|
||||
|
||||
|
||||
def load_specs(spec_dir=SPEC_DIR):
|
||||
"""Every parseable spec; a file that doesn't parse (someone mid-edit) is skipped with a warning,
|
||||
and a spec with a top-level "skip": true (a rejected body) is left out entirely."""
|
||||
specs = []
|
||||
for path in sorted(Path(spec_dir).glob("*.json")):
|
||||
spec = json.loads(path.read_text())
|
||||
try:
|
||||
spec = json.loads(path.read_text())
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"warning: skipping {path}: {e}", file=sys.stderr)
|
||||
continue
|
||||
if spec.get("skip"):
|
||||
continue
|
||||
spec["_path"] = str(path)
|
||||
specs.append(spec)
|
||||
return specs
|
||||
@@ -67,8 +77,9 @@ def _build(specs, parts_dir=PARTS_DIR):
|
||||
if p.get("skip"):
|
||||
continue
|
||||
if p.get("fixed") and (p.get("other", 0) or not p.get("types")):
|
||||
raise SpecError(f"{s['_path']}: {p['name']} uses fixed colors '{p['fixed']}' and must be "
|
||||
f"type-locked: give it 'types' and other=0")
|
||||
print(f"warning: {s['_path']}: {p['name']} uses fixed colors '{p['fixed']}' but isn't type-locked "
|
||||
f"(needs 'types' and other=0); left out", file=sys.stderr)
|
||||
continue
|
||||
if not (Path(parts_dir) / p["slot"] / f"{p['name']}.png").exists():
|
||||
continue
|
||||
palette = "fixed" if p.get("fixed") else p.get("palette", "main")
|
||||
@@ -83,13 +94,18 @@ def _build(specs, parts_dir=PARTS_DIR):
|
||||
motifs[p["name"]] = p["motif"]
|
||||
for b in p.get("deny_bodies", []):
|
||||
deny_from_parts.setdefault(b, []).append(p["name"])
|
||||
if not (Path(parts_dir) / "body" / f"{name}.png").exists():
|
||||
continue
|
||||
heads = [s["head"]] if isinstance(s.get("head"), str) else list(s.get("head", []))
|
||||
closed = [f"_slot_{slot}" for slot in FEATURE_SLOTS if slot not in s.get("slots", FEATURE_SLOTS)]
|
||||
body.append(part(
|
||||
name, [s["plan"]], s.get("types"), s.get("other", 1.0), tags=heads + list(s.get("tags", [])),
|
||||
excludes=list(s.get("excludes", [])) + closed, deny=list(s.get("deny", [])), desc=s.get("desc", "")))
|
||||
# The body and its variants (edits of it: same idea, different look) share plan, head and tags.
|
||||
for b in [s] + [v for v in s.get("variants", []) if not v.get("skip")]:
|
||||
if not (Path(parts_dir) / "body" / f"{b['name']}.png").exists():
|
||||
continue
|
||||
entry = part(
|
||||
b["name"], [s["plan"]], b.get("types", s.get("types")), b.get("other", s.get("other", BODY_OTHER)),
|
||||
tags=heads + list(s.get("tags", [])), excludes=list(s.get("excludes", [])) + closed,
|
||||
deny=list(s.get("deny", [])), desc=b.get("desc", s.get("desc", "")))
|
||||
entry["family"] = name
|
||||
body.append(entry)
|
||||
for b in body:
|
||||
b["deny"] += deny_from_parts.get(b["name"], [])
|
||||
table = {"body": body, "aura": []}
|
||||
@@ -102,9 +118,13 @@ SLOTS, MOTIFS = _build(load_specs())
|
||||
BODY = SLOTS["body"]
|
||||
MOTIF_PAIRS = set(CLASSIC_MOTIF_PAIRS)
|
||||
|
||||
_present = {b["plans"][0] for b in BODY}
|
||||
PLAN_WEIGHTS = {t: {plan: CLASSIC_PLAN_WEIGHTS[t].get(plan, 0) + PLAN_FLOOR for plan in PLANS if plan in _present}
|
||||
for t in TYPES}
|
||||
# Plan weight = total weight of that plan's bodies for the type, so picking a plan and then a body
|
||||
# within it gives every body a chance proportional to its own weight.
|
||||
PLAN_WEIGHTS = {t: {} for t in TYPES}
|
||||
for _b in BODY:
|
||||
for _t in TYPES:
|
||||
_w = _b["types"].get(_t, _b["other"])
|
||||
PLAN_WEIGHTS[_t][_b["plans"][0]] = PLAN_WEIGHTS[_t].get(_b["plans"][0], 0) + _w
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ canvas fit + attachment
|
||||
|
||||
Reference in New Issue
Block a user