diff --git a/.gitignore b/.gitignore index 7135c97..37d81fd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ # OS noise .DS_Store + +# Generated placeholder sprites (regenerate via tools/aseprite/) +assets/sprites_ph/ diff --git a/tools/aseprite/gen_sprites.lua b/tools/aseprite/gen_sprites.lua new file mode 100644 index 0000000..e74f552 --- /dev/null +++ b/tools/aseprite/gen_sprites.lua @@ -0,0 +1,73 @@ +-- Headless Aseprite sprite generator for U R M03 placeholder sprites. +-- +-- Run from the repo root: +-- aseprite -b --script tools/aseprite/gen_sprites.lua +-- Optional params: +-- aseprite -b --script-param out=assets/sprites_ph \ +-- --script-param maps=tools/aseprite/maps \ +-- --script tools/aseprite/gen_sprites.lua +-- +-- Source of truth is the text pixel maps in tools/aseprite/maps/ +-- (shared with tools/aseprite/preview.py, which renders identical +-- PNGs without Aseprite). Outputs .aseprite + .png per map. +-- All output is _ph placeholder-grade per the asset policy. + +local params = app.params or {} +local MAPS_DIR = params["maps"] or "tools/aseprite/maps" +local OUT_DIR = params["out"] or "assets/sprites_ph" + +local SPRITES = { "m03_ph", "grunt_ph", "burster_ph", "slug_ph", "scrap_ph" } + +local function parse_palette(path) + local pal = {} + for line in io.lines(path) do + local ch, hex = line:match("^(%S)%s+(%x%x%x%x%x%x)%s*$") + if ch and ch ~= "#" then + local v = tonumber(hex, 16) + pal[ch] = { + r = math.floor(v / 65536) % 256, + g = math.floor(v / 256) % 256, + b = v % 256, + } + end + end + return pal +end + +local function parse_map(path) + local rows = {} + for line in io.lines(path) do + if #line > 0 then + rows[#rows + 1] = line + assert(#line == #rows[1], + path .. ": row " .. #rows .. " width " .. #line .. " != " .. #rows[1]) + end + end + return rows +end + +if app.fs and app.fs.makeAllDirectories then + app.fs.makeAllDirectories(OUT_DIR) +end + +local pal = parse_palette(MAPS_DIR .. "/palette.txt") + +for _, name in ipairs(SPRITES) do + local rows = parse_map(MAPS_DIR .. "/" .. name .. ".txt") + local w, h = #rows[1], #rows + local spr = Sprite(w, h, ColorMode.RGB) + local img = spr.cels[1].image + for y, row in ipairs(rows) do + for x = 1, w do + local ch = row:sub(x, x) + if ch ~= "." then + local c = assert(pal[ch], name .. ": unknown palette char '" .. ch .. "'") + img:putPixel(x - 1, y - 1, app.pixelColor.rgba(c.r, c.g, c.b, 255)) + end + end + end + spr:saveAs(OUT_DIR .. "/" .. name .. ".aseprite") + spr:saveCopyAs(OUT_DIR .. "/" .. name .. ".png") + spr:close() + print("wrote " .. OUT_DIR .. "/" .. name .. ".aseprite (+.png) " .. w .. "x" .. h) +end diff --git a/tools/aseprite/maps/burster_ph.txt b/tools/aseprite/maps/burster_ph.txt new file mode 100644 index 0000000..02ee5d4 --- /dev/null +++ b/tools/aseprite/maps/burster_ph.txt @@ -0,0 +1,20 @@ +.................... +.....KKKKKKKKKK..... +....KRRRRRRRRRRK.... +...KRRRRRRRRRRRRK... +..KRRRRRRRRRRRRRRK.. +..KRRERRRRRRRRERRK.. +..KRRRRRRRRRRRRRRK.. +..KRRRGGGGGGGGRRRK.. +..KRRGGGGGGGGGGRRK.. +..KRRRGGGGGGGGRRRK.. +..KRRRRRRRRRRRRRRK.. +..KrRRRRRRRRRRRRrK.. +...KrRRRRRRRRRRrK... +....KrrRRRRRRrrK.... +.....KrrrrrrrrK..... +......KKKKKKKK...... +......KrK..KrK...... +......KKK..KKK...... +.................... +.................... diff --git a/tools/aseprite/maps/grunt_ph.txt b/tools/aseprite/maps/grunt_ph.txt new file mode 100644 index 0000000..b27cbe0 --- /dev/null +++ b/tools/aseprite/maps/grunt_ph.txt @@ -0,0 +1,16 @@ +................ +....KKKKKKKK.... +...KOOOOOOOOK... +..KOOOOOOOOOOK.. +..KOOEOOOOEOOK.. +..KOOOOOOOOOOK.. +..KOOOOOOOOOOK.. +..KoOOOOOOOOoK.. +..KooOOOOOOooK.. +...KooooooooK... +....KKKKKKKK.... +....KoK..KoK.... +....KKK..KKK.... +................ +................ +................ diff --git a/tools/aseprite/maps/m03_ph.txt b/tools/aseprite/maps/m03_ph.txt new file mode 100644 index 0000000..f54038b --- /dev/null +++ b/tools/aseprite/maps/m03_ph.txt @@ -0,0 +1,32 @@ +................................ +................................ +................................ +............KKKKKKKK............ +..........KKSSSSSSSSKK.......... +.........KSSSSSSSSSSSSK......... +........KSSSSSSSSSSSSSSK........ +.......KSSSSSSSSSSSSSSSSK....... +.......KSSSSSSSSSSSSSSSSK....... +.......KSSsEsSSSSSSWWYSSK....... +.......KSSsssSSSSSSWWYSSK....... +.......KSSSSSSSSSSSYYYSSK....... +.......KSSSSSSSSSSSSSSSSKKKKKK.. +.......KSSSSSSSSSSSSSSSSKRRRRK.. +.......KSSSSSSSSSSSSSSSSKRRRBK.. +.......KSSSSSSSSSSSSSSSSKKKKKK.. +.......KsSSSSSSSSSSSSSSsK....... +.......KsSSSSSSSSSSSSSSsK....... +........KsSSSSSSSSSSSSsK........ +........KssSSSSSSSSSSssK........ +.........KssSSSSSSSSssK......... +..........KssssssssssK.......... +...........KKKKKKKKKK........... +........KKKKKK....KKKKKK........ +........KssssK....KssssK........ +........KssssK....KssssK........ +........KssssK....KssssK........ +........KooooK....KooooK........ +.......KKooooKK..KKooooKK....... +.......KKKKKKKK..KKKKKKKK....... +................................ +................................ diff --git a/tools/aseprite/maps/palette.txt b/tools/aseprite/maps/palette.txt new file mode 100644 index 0000000..4261be3 --- /dev/null +++ b/tools/aseprite/maps/palette.txt @@ -0,0 +1,13 @@ +# Shared placeholder palette — char -> RRGGBB hex. '.' = transparent. +K 1a1e22 +S d2d6e0 +s 8a93a6 +O 73775f +o 4a4d3f +R c9743a +r 8f4a22 +G f2a24a +Y f2cc59 +B c0d9f2 +W f5f7fa +E 2e3440 diff --git a/tools/aseprite/maps/scrap_ph.txt b/tools/aseprite/maps/scrap_ph.txt new file mode 100644 index 0000000..f724bae --- /dev/null +++ b/tools/aseprite/maps/scrap_ph.txt @@ -0,0 +1,8 @@ +...KK... +..KYYK.. +.KYYYYK. +KYWYYYYK +.KYYYYK. +..KYYK.. +...KK... +........ diff --git a/tools/aseprite/maps/slug_ph.txt b/tools/aseprite/maps/slug_ph.txt new file mode 100644 index 0000000..17a567e --- /dev/null +++ b/tools/aseprite/maps/slug_ph.txt @@ -0,0 +1,3 @@ +.KKKKKK. +KWWWWBBK +.KKKKKK. diff --git a/tools/aseprite/preview.py b/tools/aseprite/preview.py new file mode 100644 index 0000000..df07b00 --- /dev/null +++ b/tools/aseprite/preview.py @@ -0,0 +1,70 @@ +"""Render the shared pixel maps to PNG without Aseprite. + +Same source of truth as gen_sprites.lua (tools/aseprite/maps/); use +this for headless preview on boxes with no Aseprite. Output is +_ph placeholder-grade per the asset policy. + + uv run --with pillow python tools/aseprite/preview.py \ + [--maps tools/aseprite/maps] [--out assets/sprites_ph] [--zoom 8] + +Writes .png (1x) and, with --zoom N > 1, @Nx.png +(nearest-neighbor upscale). +""" + +import argparse +from pathlib import Path + +from PIL import Image + +SPRITES = ["m03_ph", "grunt_ph", "burster_ph", "slug_ph", "scrap_ph"] + + +def parse_palette(path: Path) -> dict[str, tuple[int, int, int, int]]: + pal = {} + for line in path.read_text().splitlines(): + parts = line.split() + if len(parts) == 2 and len(parts[0]) == 1 and parts[0] != "#": + v = int(parts[1], 16) + pal[parts[0]] = ((v >> 16) & 255, (v >> 8) & 255, v & 255, 255) + return pal + + +def parse_map(path: Path) -> list[str]: + rows = [line for line in path.read_text().splitlines() if line] + for i, row in enumerate(rows): + if len(row) != len(rows[0]): + raise ValueError(f"{path}: row {i + 1} width {len(row)} != {len(rows[0])}") + return rows + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--maps", default="tools/aseprite/maps") + ap.add_argument("--out", default="assets/sprites_ph") + ap.add_argument("--zoom", type=int, default=8) + args = ap.parse_args() + + maps_dir = Path(args.maps) + out_dir = Path(args.out) + out_dir.mkdir(parents=True, exist_ok=True) + pal = parse_palette(maps_dir / "palette.txt") + + for name in SPRITES: + rows = parse_map(maps_dir / f"{name}.txt") + w, h = len(rows[0]), len(rows) + img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + for y, row in enumerate(rows): + for x, ch in enumerate(row): + if ch != ".": + if ch not in pal: + raise ValueError(f"{name}: unknown palette char {ch!r}") + img.putpixel((x, y), pal[ch]) + img.save(out_dir / f"{name}.png") + if args.zoom > 1: + big = img.resize((w * args.zoom, h * args.zoom), Image.Resampling.NEAREST) + big.save(out_dir / f"{name}@{args.zoom}x.png") + print(f"wrote {out_dir}/{name}.png {w}x{h}") + + +if __name__ == "__main__": + main()