feat(tools): Aseprite sprite pipeline — shared pixel maps, Lua generator, headless previewer
tools/aseprite/maps/ holds the source of truth: char-grid pixel maps + shared palette for five _ph placeholder sprites (M03 32px per MOE.md design law cues — round silhouette, mismatched eyes, bolted hardpoint, pillar feet; grunt 16px untagged olive; burster 20px HEAT glow band; KINETIC slug; scrap diamond). Two interchangeable renderers: gen_sprites.lua (aseprite -b, emits .aseprite + .png — run on any machine with Aseprite) and preview.py (Pillow, identical PNGs headless + nearest-neighbor zoom for review). Outputs gitignored; greybox scenes untouched — art stays out of the game per the greybox rule.
This commit is contained in:
@@ -4,3 +4,6 @@
|
||||
|
||||
# OS noise
|
||||
.DS_Store
|
||||
|
||||
# Generated placeholder sprites (regenerate via tools/aseprite/)
|
||||
assets/sprites_ph/
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,20 @@
|
||||
....................
|
||||
.....KKKKKKKKKK.....
|
||||
....KRRRRRRRRRRK....
|
||||
...KRRRRRRRRRRRRK...
|
||||
..KRRRRRRRRRRRRRRK..
|
||||
..KRRERRRRRRRRERRK..
|
||||
..KRRRRRRRRRRRRRRK..
|
||||
..KRRRGGGGGGGGRRRK..
|
||||
..KRRGGGGGGGGGGRRK..
|
||||
..KRRRGGGGGGGGRRRK..
|
||||
..KRRRRRRRRRRRRRRK..
|
||||
..KrRRRRRRRRRRRRrK..
|
||||
...KrRRRRRRRRRRrK...
|
||||
....KrrRRRRRRrrK....
|
||||
.....KrrrrrrrrK.....
|
||||
......KKKKKKKK......
|
||||
......KrK..KrK......
|
||||
......KKK..KKK......
|
||||
....................
|
||||
....................
|
||||
@@ -0,0 +1,16 @@
|
||||
................
|
||||
....KKKKKKKK....
|
||||
...KOOOOOOOOK...
|
||||
..KOOOOOOOOOOK..
|
||||
..KOOEOOOOEOOK..
|
||||
..KOOOOOOOOOOK..
|
||||
..KOOOOOOOOOOK..
|
||||
..KoOOOOOOOOoK..
|
||||
..KooOOOOOOooK..
|
||||
...KooooooooK...
|
||||
....KKKKKKKK....
|
||||
....KoK..KoK....
|
||||
....KKK..KKK....
|
||||
................
|
||||
................
|
||||
................
|
||||
@@ -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.......
|
||||
................................
|
||||
................................
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
...KK...
|
||||
..KYYK..
|
||||
.KYYYYK.
|
||||
KYWYYYYK
|
||||
.KYYYYK.
|
||||
..KYYK..
|
||||
...KK...
|
||||
........
|
||||
@@ -0,0 +1,3 @@
|
||||
.KKKKKK.
|
||||
KWWWWBBK
|
||||
.KKKKKK.
|
||||
@@ -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 <name>.png (1x) and, with --zoom N > 1, <name>@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()
|
||||
Reference in New Issue
Block a user