diff --git a/assets/_sources/blob_rect_grass/collision.json b/assets/_sources/blob_rect_grass/collision.json new file mode 100644 index 0000000..f6356e4 --- /dev/null +++ b/assets/_sources/blob_rect_grass/collision.json @@ -0,0 +1,270 @@ +{ + "schema": "blob-14", + "tile_size_px": 64, + "sub_cell_px": 16, + "shape_kind": "rect_list", + "_note": "Collision matches the rectilinear visual exactly. For diagonal_cut/organic/concave styles, the visible boundary deviates by up to 5 px at convex external corners \u2014 acceptable for typical gameplay collision. Modders needing pixel-perfect physics ship per-style polygon overrides.", + "slots": [ + { + "slot": 0, + "name": "isolated", + "bitmask": "0b00000000", + "rects": [ + [ + 16, + 16, + 32, + 32 + ] + ] + }, + { + "slot": 1, + "name": "end", + "bitmask": "0b00000001", + "rects": [ + [ + 16, + 0, + 32, + 48 + ] + ] + }, + { + "slot": 2, + "name": "corner_open", + "bitmask": "0b00000101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 16, + 16, + 48, + 32 + ] + ] + }, + { + "slot": 3, + "name": "corner_full", + "bitmask": "0b00000111", + "rects": [ + [ + 16, + 0, + 48, + 48 + ] + ] + }, + { + "slot": 4, + "name": "straight", + "bitmask": "0b00010001", + "rects": [ + [ + 16, + 0, + 32, + 64 + ] + ] + }, + { + "slot": 5, + "name": "tee_open", + "bitmask": "0b00010101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 16, + 16, + 48, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 6, + "name": "tee_half", + "bitmask": "0b00010111", + "rects": [ + [ + 16, + 0, + 48, + 48 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 7, + "name": "tee_full", + "bitmask": "0b00011111", + "rects": [ + [ + 16, + 0, + 48, + 64 + ] + ] + }, + { + "slot": 8, + "name": "cross_open", + "bitmask": "0b01010101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 9, + "name": "cross_q1", + "bitmask": "0b01010111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 10, + "name": "cross_q2adj", + "bitmask": "0b01011111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 48, + 16 + ] + ] + }, + { + "slot": 11, + "name": "cross_q2opp", + "bitmask": "0b01110111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 0, + 48, + 48, + 16 + ] + ] + }, + { + "slot": 12, + "name": "cross_q3", + "bitmask": "0b01111111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 48 + ] + ] + }, + { + "slot": 13, + "name": "solid", + "bitmask": "0b11111111", + "rects": [ + [ + 0, + 0, + 64, + 64 + ] + ] + } + ], + "style": "rectilinear" +} \ No newline at end of file diff --git a/assets/_sources/blob_rect_stone/collision.json b/assets/_sources/blob_rect_stone/collision.json new file mode 100644 index 0000000..f6356e4 --- /dev/null +++ b/assets/_sources/blob_rect_stone/collision.json @@ -0,0 +1,270 @@ +{ + "schema": "blob-14", + "tile_size_px": 64, + "sub_cell_px": 16, + "shape_kind": "rect_list", + "_note": "Collision matches the rectilinear visual exactly. For diagonal_cut/organic/concave styles, the visible boundary deviates by up to 5 px at convex external corners \u2014 acceptable for typical gameplay collision. Modders needing pixel-perfect physics ship per-style polygon overrides.", + "slots": [ + { + "slot": 0, + "name": "isolated", + "bitmask": "0b00000000", + "rects": [ + [ + 16, + 16, + 32, + 32 + ] + ] + }, + { + "slot": 1, + "name": "end", + "bitmask": "0b00000001", + "rects": [ + [ + 16, + 0, + 32, + 48 + ] + ] + }, + { + "slot": 2, + "name": "corner_open", + "bitmask": "0b00000101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 16, + 16, + 48, + 32 + ] + ] + }, + { + "slot": 3, + "name": "corner_full", + "bitmask": "0b00000111", + "rects": [ + [ + 16, + 0, + 48, + 48 + ] + ] + }, + { + "slot": 4, + "name": "straight", + "bitmask": "0b00010001", + "rects": [ + [ + 16, + 0, + 32, + 64 + ] + ] + }, + { + "slot": 5, + "name": "tee_open", + "bitmask": "0b00010101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 16, + 16, + 48, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 6, + "name": "tee_half", + "bitmask": "0b00010111", + "rects": [ + [ + 16, + 0, + 48, + 48 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 7, + "name": "tee_full", + "bitmask": "0b00011111", + "rects": [ + [ + 16, + 0, + 48, + 64 + ] + ] + }, + { + "slot": 8, + "name": "cross_open", + "bitmask": "0b01010101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 9, + "name": "cross_q1", + "bitmask": "0b01010111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 10, + "name": "cross_q2adj", + "bitmask": "0b01011111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 48, + 16 + ] + ] + }, + { + "slot": 11, + "name": "cross_q2opp", + "bitmask": "0b01110111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 0, + 48, + 48, + 16 + ] + ] + }, + { + "slot": 12, + "name": "cross_q3", + "bitmask": "0b01111111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 48 + ] + ] + }, + { + "slot": 13, + "name": "solid", + "bitmask": "0b11111111", + "rects": [ + [ + 0, + 0, + 64, + 64 + ] + ] + } + ], + "style": "rectilinear" +} \ No newline at end of file diff --git a/assets/_sources/blob_rect_wood/collision.json b/assets/_sources/blob_rect_wood/collision.json new file mode 100644 index 0000000..f6356e4 --- /dev/null +++ b/assets/_sources/blob_rect_wood/collision.json @@ -0,0 +1,270 @@ +{ + "schema": "blob-14", + "tile_size_px": 64, + "sub_cell_px": 16, + "shape_kind": "rect_list", + "_note": "Collision matches the rectilinear visual exactly. For diagonal_cut/organic/concave styles, the visible boundary deviates by up to 5 px at convex external corners \u2014 acceptable for typical gameplay collision. Modders needing pixel-perfect physics ship per-style polygon overrides.", + "slots": [ + { + "slot": 0, + "name": "isolated", + "bitmask": "0b00000000", + "rects": [ + [ + 16, + 16, + 32, + 32 + ] + ] + }, + { + "slot": 1, + "name": "end", + "bitmask": "0b00000001", + "rects": [ + [ + 16, + 0, + 32, + 48 + ] + ] + }, + { + "slot": 2, + "name": "corner_open", + "bitmask": "0b00000101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 16, + 16, + 48, + 32 + ] + ] + }, + { + "slot": 3, + "name": "corner_full", + "bitmask": "0b00000111", + "rects": [ + [ + 16, + 0, + 48, + 48 + ] + ] + }, + { + "slot": 4, + "name": "straight", + "bitmask": "0b00010001", + "rects": [ + [ + 16, + 0, + 32, + 64 + ] + ] + }, + { + "slot": 5, + "name": "tee_open", + "bitmask": "0b00010101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 16, + 16, + 48, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 6, + "name": "tee_half", + "bitmask": "0b00010111", + "rects": [ + [ + 16, + 0, + 48, + 48 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 7, + "name": "tee_full", + "bitmask": "0b00011111", + "rects": [ + [ + 16, + 0, + 48, + 64 + ] + ] + }, + { + "slot": 8, + "name": "cross_open", + "bitmask": "0b01010101", + "rects": [ + [ + 16, + 0, + 32, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 9, + "name": "cross_q1", + "bitmask": "0b01010111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 32, + 16 + ] + ] + }, + { + "slot": 10, + "name": "cross_q2adj", + "bitmask": "0b01011111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 16, + 48, + 48, + 16 + ] + ] + }, + { + "slot": 11, + "name": "cross_q2opp", + "bitmask": "0b01110111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 32 + ], + [ + 0, + 48, + 48, + 16 + ] + ] + }, + { + "slot": 12, + "name": "cross_q3", + "bitmask": "0b01111111", + "rects": [ + [ + 16, + 0, + 48, + 16 + ], + [ + 0, + 16, + 64, + 48 + ] + ] + }, + { + "slot": 13, + "name": "solid", + "bitmask": "0b11111111", + "rects": [ + [ + 0, + 0, + 64, + 64 + ] + ] + } + ], + "style": "rectilinear" +} \ No newline at end of file diff --git a/scripts/atlas_pack.py b/scripts/atlas_pack.py deleted file mode 100644 index ae3158e..0000000 --- a/scripts/atlas_pack.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -"""atlas_pack.py - minimal pure-Python substitute for sporel-tool-atlas-baker. - -Packs a directory of _diffuse.png sources into the Sporel runtime -atlas format expected by lib-core.maps: - - / - tiles.atlas.json ← metadata (atlas_id, atlas_size_px, tile_size_px, tiles[]) - tiles.atlas.lock.json ← name → id binding + next_id - tiles.diffuse.atlas.png ← packed RGBA atlas - tiles.height.atlas.png ← 1x1 transparent placeholder (engine reads via pcall) - -Tile entries get sequential integer IDs starting at 1, alphabetical by -source filename for stable ordering. Each tile may carry walkable + -optional `name_to_walkable` overrides via the API. -""" - -import json -import os -from typing import Callable, Optional - -from PIL import Image - - -def _pad_edge_replicate(sprite: Image.Image, p: int) -> Image.Image: - """Wrap a sprite with `p` pixels of edge-replicated padding on all - sides. The 4 edges + 4 corners are filled by stretching a 1-pixel - strip outward (NEAREST resampling). This prevents bilinear sampling - near tile boundaries from leaking into neighbouring atlas tiles.""" - if p <= 0: - return sprite - w, h = sprite.size - pw, ph = w + 2 * p, h + 2 * p - new = Image.new(sprite.mode, (pw, ph), (0, 0, 0, 0)) - new.paste(sprite, (p, p)) - # 4 edges. - top = sprite.crop((0, 0, w, 1)).resize((w, p), Image.Resampling.NEAREST) - new.paste(top, (p, 0)) - bot = sprite.crop((0, h - 1, w, h)).resize((w, p), Image.Resampling.NEAREST) - new.paste(bot, (p, h + p)) - left = sprite.crop((0, 0, 1, h)).resize((p, h), Image.Resampling.NEAREST) - new.paste(left, (0, p)) - right = sprite.crop((w - 1, 0, w, h)).resize((p, h), Image.Resampling.NEAREST) - new.paste(right, (w + p, p)) - # 4 corners. - for (sx, sy, dx, dy) in [ - (0, 0, 0, 0), - (w - 1, 0, w + p, 0), - (0, h - 1, 0, h + p), - (w - 1, h - 1, w + p, h + p), - ]: - corner = sprite.crop((sx, sy, sx + 1, sy + 1)).resize((p, p), Image.Resampling.NEAREST) - new.paste(corner, (dx, dy)) - return new - - -def pack( - source_dir: str, - out_dir: str, - atlas_id: str, - tile_size_px: int, - walkable_fn: Optional[Callable[[str], bool]] = None, - cols: Optional[int] = None, - pad_px: int = 2, -) -> None: - """Pack all _diffuse.png in source_dir into a single atlas. - - walkable_fn(tile_name) -> bool decides per-tile walkability; defaults - to False if not supplied (safe for wall-like tiles, override for - floors). cols controls grid width; default = ceil(sqrt(N)). - - pad_px: edge-replicated padding around each tile in the packed - atlas. Prevents bilinear-filtering bleed between neighbouring atlas - tiles when sprites are rendered near their UV boundary. UV coords - in the metadata still point to the inner non-padded region. - """ - sources = sorted( - f for f in os.listdir(source_dir) if f.endswith("_diffuse.png") - ) - if not sources: - raise RuntimeError(f"no _diffuse.png files in {source_dir}") - - n = len(sources) - if cols is None: - import math - cols = math.ceil(math.sqrt(n)) - rows = (n + cols - 1) // cols - stride = tile_size_px + 2 * pad_px # per-tile cell stride including padding - atlas_w = cols * stride - atlas_h = rows * stride - - atlas = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0)) - - tiles_meta = [] - bindings = {} - - for idx, fname in enumerate(sources): - name = fname[: -len("_diffuse.png")] - col = idx % cols - row = idx // cols - cell_x = col * stride - cell_y = row * stride - - sprite = Image.open(os.path.join(source_dir, fname)).convert("RGBA") - if sprite.size != (tile_size_px, tile_size_px): - sprite = sprite.resize((tile_size_px, tile_size_px), resample=Image.Resampling.NEAREST) - padded = _pad_edge_replicate(sprite, pad_px) - atlas.paste(padded, (cell_x, cell_y)) - - # UV points to the inner non-padded region. - uv_x = cell_x + pad_px - uv_y = cell_y + pad_px - tile_id = idx + 1 - bindings[name] = tile_id - walkable = walkable_fn(name) if walkable_fn else False - tiles_meta.append({ - "id": tile_id, - "name": name, - "uv": [uv_x, uv_y, tile_size_px, tile_size_px], - "walkable": walkable, - }) - - os.makedirs(out_dir, exist_ok=True) - atlas.save(os.path.join(out_dir, "tiles.diffuse.atlas.png")) - - # Height placeholder: 1x1 transparent. Engine reads via pcall and tolerates absence, - # but shipping a real PNG avoids a confusing "asset load failed" warning in logs. - Image.new("RGBA", (1, 1), (0, 0, 0, 0)).save( - os.path.join(out_dir, "tiles.height.atlas.png") - ) - - atlas_json = { - "atlas_id": atlas_id, - "atlas_version": 1, - "atlas_size_px": [atlas_w, atlas_h], - "tile_size_px": tile_size_px, - "tiles": tiles_meta, - } - with open(os.path.join(out_dir, "tiles.atlas.json"), "w", encoding="utf-8") as f: - json.dump(atlas_json, f, indent=2) - - lock_json = { - "atlas_id": atlas_id, - "bindings": bindings, - "deleted": [], - "next_id": n + 1, - } - with open(os.path.join(out_dir, "tiles.atlas.lock.json"), "w", encoding="utf-8") as f: - json.dump(lock_json, f, indent=2) - - print( - f" packed {n} tiles ({cols}x{rows} grid, pad={pad_px}px, " - f"{atlas_w}x{atlas_h} px) -> {out_dir}" - ) diff --git a/scripts/bake.sh b/scripts/bake.sh index f31f76e..9032004 100644 --- a/scripts/bake.sh +++ b/scripts/bake.sh @@ -1,25 +1,48 @@ #!/usr/bin/env bash -# bake.sh - regenerate procedural source sprites + reference sheets, -# then pack each of the 4 style atlases via sporel-tool-atlas-baker. -# -# Idempotent: re-running overwrites all generated outputs. +# bake.sh - regenerate source PNGs (+ collision.json + reference sheets + +# color showcase) via Python, then pack each atlas via sporel-tool- +# atlas-baker (Node). The node baker validates blob-14 schema for the +# single-material atlases (E1) + collision.json sidecar (E2) + +# computes per-tile opaque flag via alpha-analysis. Atlases ship with +# 2 px edge-replicated padding (Rev 4 §13.3) to prevent bilinear bleed. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" BAKER=~/Projects/Sporel/sporel-tool-atlas-baker/bin/atlas-baker.js -# 1) Regenerate sources for all 4 styles (diffuse PNGs + collision.json) -# + the per-style reference sheets + the color showcase. +# Ensure node is on PATH (Windows winget-installed Node lives here). +export PATH="/c/Program Files/nodejs:$PATH" + +# 1) Regenerate sources for all 4 base styles (+ collision.json + the +# per-style reference sheets + the color showcase) via Python. python "$SCRIPT_DIR/generate.py" python "$SCRIPT_DIR/reference_sheet.py" python "$SCRIPT_DIR/color_showcase.py" +python "$SCRIPT_DIR/bake_testbench.py" +python "$SCRIPT_DIR/bake_singles.py" -# 2) Pack each style into its own runtime atlas. -for style in rectilinear diagonal_cut organic concave; do - atlas_id="blob_${style}" +# 2) Pack each atlas via the node baker. +# Single-material blob-14 atlases (4 styles + 3 tinted singles). +# All ship --schema blob-14 so E1 (14-slot validation) + E2 (collision +# sidecar validation) run + the collision.json is copied to the atlas +# output as tiles.collision.json. +for atlas_id in \ + blob_rectilinear blob_diagonal_cut blob_organic blob_concave \ + blob_rect_stone blob_rect_grass blob_rect_wood; do node "$BAKER" \ - --in "$LIB_DIR/assets/_sources/${atlas_id}" \ - --out "$LIB_DIR/assets/atlases/${atlas_id}" \ - --atlas-id "${atlas_id}" \ - --tile-size auto + --in "$LIB_DIR/assets/_sources/${atlas_id}" \ + --out "$LIB_DIR/assets/atlases/${atlas_id}" \ + --atlas-id "${atlas_id}" \ + --tile-size 64 \ + --schema blob-14 done + +# Multi-material testbench: 3 materials × 14 slots = 42 tiles in one +# atlas. NOT --schema blob-14 (the 42-tile layout doesn't match the +# canonical 14-slot enumeration). Collision.json absent → baker +# emits no tiles.collision.json sidecar. +node "$BAKER" \ + --in "$LIB_DIR/assets/_sources/blob_testbench" \ + --out "$LIB_DIR/assets/atlases/blob_testbench" \ + --atlas-id blob_testbench \ + --tile-size 64 diff --git a/scripts/bake_singles.py b/scripts/bake_singles.py index 593321c..f427c54 100644 --- a/scripts/bake_singles.py +++ b/scripts/bake_singles.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 -"""bake_singles.py - bake single-material atlases for vertex-painted -autotile layers. Each material is one tinted 14-slot atlas, separate -from the mixed-material blob_testbench atlas. +"""bake_singles.py - emit single-material source PNGs (+ collision.json) +for vertex-painted autotile layers. Each material is one tinted 14-slot +source-dir; the node atlas-baker packs them into atlases via bake.sh. -Atlases produced (under assets/atlases/blob_rect_/): +Atlases produced (via the downstream baker invocation): - blob_rect_stone (dark gray, walls/floors) - blob_rect_grass (mid green, organic) - blob_rect_wood (light brown, paths) -Filenames inside each atlas use the canonical slot_NN_ naming so -the runtime can build a slot->tile_id index by parsing tile names. +Source dirs (this script's output): assets/_sources/blob_rect_/ +contain 14 PNGs named slot_NN__diffuse.png plus a copy of +the shared collision.json (rectilinear geometry, identical across +materials since they share the same blob shape). """ import os +import shutil from PIL import Image from generate import SLOTS, render_slot, TILE_PX # noqa: E402 -from atlas_pack import pack # noqa: E402 MATERIALS = [ ("stone", (40, 44, 56)), @@ -43,10 +45,18 @@ def tint(img: Image.Image, rgb: tuple[int, int, int]) -> Image.Image: def main() -> None: script_dir = os.path.dirname(os.path.abspath(__file__)) lib_dir = os.path.normpath(os.path.join(script_dir, "..")) + # Shared collision.json source (written by generate.py for rectilinear). + shared_collision = os.path.join( + lib_dir, "assets", "_sources", "blob_rectilinear", "collision.json" + ) + if not os.path.exists(shared_collision): + raise RuntimeError( + f"missing {shared_collision} — run generate.py first" + ) + for material, rgb in MATERIALS: atlas_id = f"blob_rect_{material}" src_dir = os.path.join(lib_dir, "assets", "_sources", atlas_id) - out_dir = os.path.join(lib_dir, "assets", "atlases", atlas_id) os.makedirs(src_dir, exist_ok=True) # Emit 14 tinted source PNGs with canonical slot_NN naming. @@ -55,22 +65,11 @@ def main() -> None: fname = f"slot_{slot:02d}_{slot_name}_diffuse.png" img.save(os.path.join(src_dir, fname)) - # Walkability: slot_00 (isolated) is non-walkable for stone (decorative - # pillar); all others material-dependent. Stone = all non-walkable - # (walls). Grass/wood = walkable except isolated. - def walk_fn(name, _mat=material): - if _mat == "stone": - return False - return "_isolated" not in name + # Copy the shared rectilinear collision.json so the atlas-baker + # E2 validation finds it sibling to the diffuse sources. + shutil.copyfile(shared_collision, os.path.join(src_dir, "collision.json")) - pack( - source_dir=src_dir, - out_dir=out_dir, - atlas_id=atlas_id, - tile_size_px=TILE_PX, - walkable_fn=walk_fn, - cols=14, # one row - ) + print(f" wrote {len(SLOTS)} sprites + collision.json -> {src_dir}") if __name__ == "__main__": diff --git a/scripts/bake_testbench.py b/scripts/bake_testbench.py index e697073..78964b2 100644 --- a/scripts/bake_testbench.py +++ b/scripts/bake_testbench.py @@ -16,7 +16,6 @@ import os from PIL import Image from generate import SLOTS, render_slot, TILE_PX # noqa: E402 -from atlas_pack import pack # noqa: E402 # (material_name, FG tint rgb) — same palette as color_showcase.py. TESTBENCH_MATERIALS = [ @@ -58,25 +57,15 @@ def main() -> None: os.makedirs(src_dir, exist_ok=True) - # 1) Write the 42 tinted source PNGs. + # Write the 42 tinted source PNGs. Atlas-packing is delegated to + # sporel-tool-atlas-baker (Node) — see scripts/bake.sh. print(f"Writing {len(TESTBENCH_MATERIALS) * len(SLOTS)} tinted sprites -> {src_dir}") for material, rgb in TESTBENCH_MATERIALS: for slot, slot_name, pattern in SLOTS: img = tinted(pattern, rgb) fname = f"{material}_slot_{slot:02d}_{slot_name}_diffuse.png" img.save(os.path.join(src_dir, fname)) - - # 2) Pack into the runtime atlas. Use 14 cols x 3 rows so each row is - # one material's slot strip (visually navigable when inspecting the - # baked PNG directly). - pack( - source_dir=src_dir, - out_dir=out_dir, - atlas_id=ATLAS_ID, - tile_size_px=TILE_PX, - walkable_fn=is_walkable, - cols=14, - ) + print(f"out_dir kept for reference: {out_dir}") if __name__ == "__main__":