"""Bake additional Tiny Swords assets into app/rpg_static/sprites. The original bake copied a curated subset of the raw packs in ``assets/`` into ``app/rpg_static/sprites`` + ``manifest.json``. This script adds the previously unused assets that give each biome its own identity: * terrain tiles — sand (cropped from Tilemap_Flat), snow + stone (recolors) * deco recolors — snowy / dead trees from the baked tree * static deco — colored houses, castles, towers, gold-mine states, resource piles, a fallen knight * animated — flame effect, TNT / Barrel goblins, colored wood towers Run from the repo root (needs Pillow + the raw packs in assets/):: .venv/bin/python scripts/bake_extra_sprites.py """ from __future__ import annotations import json from pathlib import Path from PIL import Image, ImageEnhance ROOT = Path(__file__).resolve().parent.parent TS = ROOT / "assets/Tiny Swords/Tiny Swords (Update 010)" OUT = ROOT / "app/rpg_static/sprites" MANIFEST = ROOT / "app/rpg_static/manifest.json" def tint(im: Image.Image, rgb: tuple[int, int, int], strength: float) -> Image.Image: """Blend ``im`` toward a flat color, preserving alpha.""" im = im.convert("RGBA") overlay = Image.new("RGBA", im.size, rgb + (0,)) overlay.putalpha(im.getchannel("A")) return Image.blend(im, overlay, strength) def recolor(src: Path, dst: Path, *, saturation: float, brightness: float, tint_rgb: tuple[int, int, int] | None = None, tint_strength: float = 0.0) -> None: im = Image.open(src).convert("RGBA") alpha = im.getchannel("A") im = ImageEnhance.Color(im).enhance(saturation) im = ImageEnhance.Brightness(im).enhance(brightness) if tint_rgb is not None and tint_strength > 0: im = tint(im, tint_rgb, tint_strength) im.putalpha(alpha) im.save(dst) print(f"recolor {dst.name}") def crop(src: Path, dst: Path, box: tuple[int, int, int, int]) -> None: Image.open(src).convert("RGBA").crop(box).save(dst) print(f"crop {dst.name} {box}") def copy(src: Path, dst: Path) -> None: Image.open(src).convert("RGBA").save(dst) print(f"copy {dst.name}") def main() -> None: flat = TS / "Terrain/Ground/Tilemap_Flat.png" # ---- terrain tiles (64x64 centers) ------------------------------------ # Tilemap_Flat: cols 0-3 grass block, cols 5-8 sand block; (6,1) is the # interior sand tile (the grass twin of the baked grass.png at (1,1)). crop(flat, OUT / "sand.png", (6 * 64, 64, 7 * 64, 128)) # No snow/stone tiles ship with the pack — derive them from sand/grass so # they keep the same pixel texture as the rest of the terrain. recolor(OUT / "sand.png", OUT / "snow.png", saturation=0.10, brightness=1.14, tint_rgb=(208, 226, 248), tint_strength=0.42) recolor(OUT / "grass.png", OUT / "stone.png", saturation=0.16, brightness=0.78, tint_rgb=(96, 96, 118), tint_strength=0.30) # ---- deco recolors ------------------------------------------------------ recolor(OUT / "deco_tree.png", OUT / "deco_tree_snow.png", saturation=0.25, brightness=1.25, tint_rgb=(210, 232, 255), tint_strength=0.30) recolor(OUT / "deco_tree.png", OUT / "deco_tree_dead.png", saturation=0.12, brightness=0.62, tint_rgb=(92, 70, 54), tint_strength=0.25) # ---- static deco copies ------------------------------------------------- deco_src = { "house_red": "Factions/Knights/Buildings/House/House_Red.png", "house_purple": "Factions/Knights/Buildings/House/House_Purple.png", "house_yellow": "Factions/Knights/Buildings/House/House_Yellow.png", "house_destroyed": "Factions/Knights/Buildings/House/House_Destroyed.png", "castle_yellow": "Factions/Knights/Buildings/Castle/Castle_Yellow.png", "castle_purple": "Factions/Knights/Buildings/Castle/Castle_Purple.png", "tower_red": "Factions/Knights/Buildings/Tower/Tower_Red.png", "tower_destroyed": "Factions/Knights/Buildings/Tower/Tower_Destroyed.png", "goldmine_destroyed": "Resources/Gold Mine/GoldMine_Destroyed.png", "goldmine_inactive": "Resources/Gold Mine/GoldMine_Inactive.png", "res_gold": "Resources/Resources/G_Idle.png", "res_wood": "Resources/Resources/W_Idle.png", "res_meat": "Resources/Resources/M_Idle.png", } for key, rel in deco_src.items(): copy(TS / rel, OUT / f"{key}.png") # Dead.png is a 7-frame 128x256 skull-bounce strip (big skull top, small # skull bottom). Frame 4's top half is the big skull at rest — a perfect # static prop for the Bone Market and battlefield areas. crop(TS / "Factions/Knights/Troops/Dead/Dead.png", OUT / "skull.png", (4 * 128, 0, 5 * 128, 128)) # ---- animated sheets (full copies; client plays row 0) ------------------ anim_src = { # key: (path, fw, fh, frames) "flame": ("Effects/Fire/Fire.png", 128, 128, 7), "tnt_goblin": ("Factions/Goblins/Troops/TNT/Red/TNT_Red.png", 192, 192, 7), "barrel_goblin": ("Factions/Goblins/Troops/Barrel/Red/Barrel_Red.png", 192, 192, 4), "wood_tower_blue": ("Factions/Goblins/Buildings/Wood_Tower/Wood_Tower_Blue.png", 128, 192, 8), "wood_tower_purple": ("Factions/Goblins/Buildings/Wood_Tower/Wood_Tower_Purple.png", 128, 192, 8), "wood_tower_yellow": ("Factions/Goblins/Buildings/Wood_Tower/Wood_Tower_Yellow.png", 128, 192, 8), } for key, (rel, *_rest) in anim_src.items(): copy(TS / rel, OUT / f"{key}.png") # ---- manifest ----------------------------------------------------------- manifest = json.loads(MANIFEST.read_text()) for key in deco_src: im = Image.open(OUT / f"{key}.png") manifest["deco"][key] = {"file": f"sprites/{key}.png", "fw": im.width, "fh": im.height} manifest["deco"]["skull"] = {"file": "sprites/skull.png", "fw": 128, "fh": 128} # tree recolors keep the baked tree's frame geometry (6 frames of 256x256; # the client draws frame 0 for deco) for key in ("tree_snow", "tree_dead"): manifest["deco"][key] = {"file": f"sprites/deco_{key}.png", "fw": 256, "fh": 256, "frames": 6} manifest["deco"].pop("dead_knight", None) # fix the pre-existing bush entry: it's an 8-frame 128x128 strip that was # declared as one 1024-wide sprite (rendered squashed). Frame 0 is correct. manifest["deco"]["bush"] = {"file": "sprites/deco_bush.png", "fw": 128, "fh": 128, "frames": 8} for key, (_rel, fw, fh, frames) in anim_src.items(): manifest["creatures"][key] = {"file": f"sprites/{key}.png", "fw": fw, "fh": fh, "frames": frames} MANIFEST.write_text(json.dumps(manifest, indent=1) + "\n") print(f"manifest updated: {len(manifest['deco'])} deco, {len(manifest['creatures'])} creatures") if __name__ == "__main__": main()