- 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
74 lines
3.5 KiB
Python
74 lines
3.5 KiB
Python
"""Offline normalization of generated door components and distance-driven gait art."""
|
|
from pathlib import Path
|
|
import struct
|
|
from PIL import Image
|
|
from pack_guest_cast import isolate
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
ART = ROOT / "assets/sprites/hall"
|
|
OUT = ROOT / "assets/animations/hall"
|
|
PREVIEWS = ROOT / "build/hall-expansion"
|
|
|
|
|
|
def pack(name, frames):
|
|
w, h = frames[0].size
|
|
assert all(frame.size == (w, h) for frame in frames)
|
|
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
|
|
(OUT / name).write_bytes(struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(payload)) + payload)
|
|
|
|
|
|
def guests():
|
|
sheet = Image.open(ART / "guest-walk-v2.png").convert("RGBA")
|
|
# The generated strip has variable row heights; these gutters separate feet/heads.
|
|
rows = (0, 190, 376, 560, 746, 916, sheet.height)
|
|
frames = []
|
|
for appearance in range(6):
|
|
cells = [isolate(sheet.crop((col*sheet.width//8, rows[appearance],
|
|
(col+1)*sheet.width//8, rows[appearance+1]))) for col in range(8)]
|
|
scale = min(126/max(cell.width for cell in cells), 204/max(cell.height for cell in cells))
|
|
for cell in cells:
|
|
cell = cell.resize((round(cell.width*scale), round(cell.height*scale)), Image.Resampling.LANCZOS)
|
|
frame = Image.new("RGBA", (128,224))
|
|
frame.alpha_composite(cell, ((128-cell.width)//2, 224-cell.height))
|
|
frames.append(frame)
|
|
pack("guest-walk-v2.bin", frames)
|
|
pack("guest-walk-left-v2.bin", [frame.transpose(Image.Transpose.FLIP_LEFT_RIGHT) for frame in frames])
|
|
preview = Image.new("RGBA", (8*128,6*224), (27,30,29,255))
|
|
for i, frame in enumerate(frames[:48]):
|
|
preview.alpha_composite(frame, ((i%8)*128, (i//8)*224))
|
|
preview.save(PREVIEWS / "guest-walk-v2-preview.png")
|
|
|
|
|
|
def door():
|
|
sheet = Image.open(ART / "basement-door-v2.png").convert("RGBA")
|
|
leaf = isolate(sheet.crop((0,0,640,sheet.height))).resize((256,512), Image.Resampling.LANCZOS)
|
|
source_frame = isolate(sheet.crop((640,0,1420,sheet.height)))
|
|
wheel = isolate(sheet.crop((1420,0,sheet.width,sheet.height))).resize((92,92), Image.Resampling.LANCZOS)
|
|
frames = []
|
|
for phase in range(17):
|
|
texture = leaf.copy()
|
|
rotated = wheel.rotate(-270*phase/16, Image.Resampling.BICUBIC, expand=False)
|
|
# UV texels are not square in world space on the 1.5 by 1.9 metre leaf.
|
|
rotated = rotated.resize((58,92), Image.Resampling.LANCZOS)
|
|
texture.alpha_composite(rotated, (99,244))
|
|
frames.append(texture)
|
|
# Map the rigid jambs around a known rectangular aperture, without a baked leaf.
|
|
w,h = source_frame.size
|
|
left, right, lintel = round(w*.24), round(w*.72), round(h*.14)
|
|
frame = Image.new("RGBA", (256,512))
|
|
frame.alpha_composite(source_frame.crop((0,0,left,h)).resize((64,512)), (0,0))
|
|
frame.alpha_composite(source_frame.crop((right,0,w,h)).resize((64,512)), (192,0))
|
|
frame.alpha_composite(source_frame.crop((left,0,right,lintel)).resize((128,32)), (64,0))
|
|
frames.append(frame)
|
|
pack("basement-door-v2.bin", frames)
|
|
preview = Image.new("RGBA", (3*256,512), (27,30,29,255))
|
|
for i, texture in enumerate((frames[0],frames[8],frame)):
|
|
preview.alpha_composite(texture,(i*256,0))
|
|
preview.save(PREVIEWS / "basement-door-v2-preview.png")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
PREVIEWS.mkdir(parents=True, exist_ok=True)
|
|
guests()
|
|
door()
|
|
print("Packed 96 gait frames and 18 door textures; runtime remains native assembly.")
|