"""Regenerate acdream's application icons into assets/icons/. py tools/IconForge/forge.py launcher # procedural, no game data needed py tools/IconForge/forge.py client # needs a mosswart mesh export py tools/IconForge/forge.py all The launcher mark is entirely procedural: a forged ring and the crescent glyph are generated from geometry in ring.py / ac_glyph.py and shaded through a matcap, so it rebuilds anywhere. The client mark renders the retail mosswart head, so it needs two inputs that come out of the installed DATs (see README.md): mosswart_mesh.json geometry export, Setup 0x02000B4F textures/0x05001E11... the skin atlas and its siblings Both default to --work, which is where those extraction steps write. """ import argparse import os import sys import numpy as np from PIL import Image, ImageChops, ImageFilter HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import ac_glyph as ag # noqa: E402 import chisel # noqa: E402 import compose # noqa: E402 import launcher as lz # noqa: E402 import render # noqa: E402 import ring # noqa: E402 import smooth # noqa: E402 REPO = os.path.abspath(os.path.join(HERE, "..", "..")) ASSETS = os.path.join(REPO, "assets", "icons") # Sizes written as loose PNGs, and the subset baked into the .ico. PNG_SIZES = (1024, 512, 256, 128, 64, 48, 32, 24, 16) ICO_SIZES = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] # The head is part 14 of the mosswart Setup; see assets/icons/README.md. HEAD_PART = 14 def _emit(master, stem): os.makedirs(ASSETS, exist_ok=True) master.save(os.path.join(ASSETS, f"{stem}-1024.png")) for s in PNG_SIZES: if s == 1024: continue master.resize((s, s), Image.LANCZOS).save(os.path.join(ASSETS, f"{stem}-{s}.png")) master.save(os.path.join(ASSETS, f"{stem}.ico"), sizes=ICO_SIZES) print(f" wrote {stem}-*.png and {stem}.ico into assets/icons/") def build_launcher(size=1024): """Forged ring + barbed crescent, on the retail icon's warm field.""" chrome = ring.chrome_matcap(768) def shade(mask, bevel, ao, exposure=1.0): return chisel.shade(ag.soften(mask, 1.2), chrome, bevel_px=int(size * bevel), profile="chisel", plateau=0.0, ao=ao, exposure=exposure) ring_img = shade(ag.thin_ring(size, R=0.86, thick=0.058), 0.026, 0.42) glyph_img = shade(ag.ac_glyph(size, R=0.68, inner_r=0.605, offset=0.275, rot_deg=-14), 0.034, 0.38, 1.06) # Glyph over ring with a soft cast shadow, so the ring reads as passing # behind the crescent the way it does in the retail wordmark. def over(base, top): a = top.split()[3].filter(ImageFilter.GaussianBlur(int(size * 0.012))) a = ImageChops.offset(a, 0, int(size * 0.006)).point(lambda v: int(v * 0.55)) sh = Image.new("RGBA", base.size, (0, 0, 0, 0)) sh.putalpha(a) return Image.alpha_composite(Image.alpha_composite(base, sh), top) sigil = over(Image.new("RGBA", (size, size), (0, 0, 0, 0)), ring_img) sigil = over(sigil, glyph_img) out, _ = lz.place(compose.field(size, "#3A3418", "#070803"), sigil, 0.94) out.putalpha(ImageChops.multiply(out.split()[3], compose.rounded_mask(size))) return out def build_client(work, size=1024): """The retail mosswart head, smoothed, lit, and badged.""" mesh_path = os.path.join(work, "mosswart_mesh.json") if not os.path.exists(mesh_path): raise SystemExit( f"missing {mesh_path}\n" "Export it first (see assets/icons/README.md) -- the client mark is\n" "rendered from the installed DATs, not from committed geometry.") textures = render.load_textures(render.all_texture_ids(mesh_path)) if not textures: raise SystemExit( "no textures resolved. Dump the mosswart surfaces first " "(see assets/icons/README.md).") raw = render.load_mesh(mesh_path, parts_filter={HEAD_PART}, ignore_transform=True) head = smooth.smooth_mesh(raw, crease_deg=52, level=6) # az/el chosen because a mosswart's eyes sit on the sides of the skull like # a frog's: dead-on frontal turns them edge-on and the face stops reading. hero = render.render( head, textures, size=size, ss=3, fit=0.90, fov=30, az=266, el=32, key_col=(0.92, 0.94, 0.76), amb_top=(0.17, 0.22, 0.14), amb_bot=(0.03, 0.04, 0.02), fill_col=(0.13, 0.19, 0.10), rim_col=(0.58, 0.80, 0.28), spec_amt=0.46, spec_pow=20.0, exposure=0.88, gamma=1.28, key_cam=(-0.45, 0.55, 0.70), fill_cam=(0.80, 0.05, 0.35), rim_cam=(0.50, 0.35, -0.62)) return compose.badge(hero, size, "rounded", occupancy=0.74, offset_y=0.02, inner="#243014", outer="#070A04") def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("target", choices=("launcher", "client", "all")) ap.add_argument("--work", default=os.path.join(HERE, "work"), help="directory holding the DAT export inputs (client only)") ap.add_argument("--size", type=int, default=1024) args = ap.parse_args() if args.target in ("launcher", "all"): print("forging launcher icon...") _emit(build_launcher(args.size), "acdream-launcher") if args.target in ("client", "all"): print("forging client icon...") render.TEXDIR = os.path.join(args.work, "textures") _emit(build_client(args.work, args.size), "acdream-client") print("done.") if __name__ == "__main__": main()