KAZIK/tools/generators/pack_fortune_room.py
Artvasil09 51c6512675 feat: implement secret fortune teller room, lived-in den v3, and bar camera stabilization
- Secret Fortune Teller Room: arcade lantern toggle, sliding timber panel, dedicated staircase and compact cellar chamber

- Lived-in den pass v3: cloth hangings, draped workbench, props/clutter, centered entrance, and custom light sources

- Bar camera stabilization: remove alcohol horizon sway while retaining drink animations

- Synchronize binaries and update launcher hash to EADBA9C8C9BD5B7E3CC4CE486ED647C1CE9383D1DCB6EBC94FE755EA1F4BF927
2026-10-20 17:29:52 +03:00

63 lines
2.9 KiB
Python

"""Pack generated tarot-room sprites. Offline tooling, never a game runtime."""
from pathlib import Path
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
def sprites(name, row_split, bottom_split=.5, top_split=.5):
source = ROOT / "assets/sprites/hall" / name
sheet = Image.open(source).convert("RGBA")
w, h = sheet.size
frames = []
# The source has tightly spaced silhouettes. Bounds are checked on alpha.
# Each cell is independently fitted, but the two idle poses share one scale.
# The generated first row ends above the bookcase's crescent crest.
split = round(h * row_split)
top = round(w * top_split)
cells = [sheet.crop((0,0,top,split)), sheet.crop((top,0,w,split)),
sheet.crop((0,split,round(w*bottom_split),h)),
sheet.crop((round(w*bottom_split),split,w,h))]
bounds = [cell.getchannel("A").point(lambda a: 255 if a > 32 else 0).getbbox() for cell in cells]
assert all(bounds), "missing transparent sprite"
cropped = [cell.crop(box) for cell, box in zip(cells, bounds)]
scale = min(242/max(c.width for c in cropped[:2]), 252/max(c.height for c in cropped[:2]))
for i, cell in enumerate(cropped):
factor = scale if i < 2 else min(242/cell.width, 252/cell.height)
cell = cell.resize((round(cell.width*factor), round(cell.height*factor)), Image.Resampling.LANCZOS)
frame = Image.new("RGBA", (256,256))
frame.alpha_composite(cell, ((256-cell.width)//2, 256-cell.height))
frames.append(frame)
return frames
def pack(name, frames):
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
target = ROOT / "assets/animations/hall" / name
target.write_bytes(struct.pack("<4s4I",b"ANM1",256,256,len(frames),len(payload))+payload)
def main():
frames = sprites("fortune-room-v2.png", .47)
frames += sprites("fortune-decor-v2.png", .52, .60)
frames += sprites("fortune-wall-v2.png", .46, top_split=.55)
frames += sprites("fortune-cabinets-v2.png", .58, top_split=.47)
frames += sprites("fortune-den-v3.png", .575, .61, top_split=.64)
frames += sprites("fortune-clutter-v3.png", .53, .55, top_split=.515)
pack("fortune-room-v3.bin", frames)
sheet = Image.open(ROOT / "assets/sprites/hall/fortune-materials-v3.png").convert("RGBA")
w, h = sheet.size
materials = [sheet.crop(box).resize((256,256), Image.Resampling.LANCZOS)
for box in ((0,0,w//2,h//2), (w//2,0,w,h//2),
(0,h//2,w//2,h), (w//2,h//2,w,h))]
pack("fortune-materials-v3.bin", materials)
preview = Image.new("RGBA", (1024,1536), (22,30,48,255))
for i, frame in enumerate(frames):
preview.alpha_composite(frame,((i%4)*256,(i//4)*256))
preview.save(ROOT / "build/hall-expansion/fortune-art-preview.png")
print("Packed twenty-four den sprites and four worn cloth/stone materials.")
if __name__ == "__main__":
main()