test-goblin-space / scripts /bake_extra_sprites.py
ASHu2's picture
Add minimap and biome visual polish to the RPG
5a28fee unverified
Raw
History Blame Contribute Delete
7.01 kB
"""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()