| """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" |
|
|
| |
| |
| |
| crop(flat, OUT / "sand.png", (6 * 64, 64, 7 * 64, 128)) |
| |
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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") |
| |
| |
| |
| crop(TS / "Factions/Knights/Troops/Dead/Dead.png", |
| OUT / "skull.png", (4 * 128, 0, 5 * 128, 128)) |
|
|
| |
| anim_src = { |
| |
| "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 = 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} |
| |
| |
| 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) |
| |
| |
| 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() |
|
|