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
|
# OS noise
|
||||||
.DS_Store
|
.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