- 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
63 lines
2.9 KiB
Python
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()
|