- Billiards: add native Russian Pyramid & American Eight-Ball pool with physics prediction, controls, and regression tests - Scratch Tickets: add dedicated counter, mat presentation, elliptical coin scraping, 4 ticket types, and payout effects - First-Person Dice: add native dice scene, hand anchor frames, throwing animation, and table perspective - City Wake & Weather: add arrival cutscene, alley geometry, weather/rain effects, and lighter/rocker animation - World Dressing: add hall paintings, furniture packs, hall lighting, and scene regression tests - Build & Tooling: update build.bat for modern MSVC toolchain, update launcher hash, and refresh shipping binaries
68 lines
2.7 KiB
Python
68 lines
2.7 KiB
Python
"""Offline normalization only; scratch masks, input and prizes run in MASM."""
|
|
from pathlib import Path
|
|
import json
|
|
import struct
|
|
from PIL import Image
|
|
import pack_city_wake
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SRC = ROOT / "assets/sprites/scratch"
|
|
OUT = ROOT / "assets/animations/scratch"
|
|
|
|
|
|
def opaque(image):
|
|
image = image.convert("RGBA")
|
|
image.putalpha(image.getchannel("A").point(lambda a: 255 if a >= 160 else 0))
|
|
return image
|
|
|
|
|
|
def pack(name, frames):
|
|
w,h = frames[0].size
|
|
raw = b"".join(f.tobytes("raw","BGRA") for f in frames)
|
|
(OUT / name).write_bytes(struct.pack("<4s4I",b"ANM1",w,h,len(frames),len(raw))+raw)
|
|
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True,exist_ok=True)
|
|
table = Image.open(SRC / "table-v1.png").convert("RGBA").resize((640,480),Image.Resampling.NEAREST)
|
|
table.putalpha(255)
|
|
pack("table-v1.bin",[table])
|
|
tickets = []
|
|
for name in ("ticket-v1.png", "ticket-numbers-v1.png", "ticket-rows-v1.png", "ticket-instant-v1.png"):
|
|
ticket = opaque(Image.open(SRC / name))
|
|
tickets.append(ticket.crop(ticket.getbbox()).resize((448,288),Image.Resampling.NEAREST))
|
|
pack("ticket-v1.bin",tickets)
|
|
foil = Image.open(SRC / "foil-v1.png").convert("RGBA").resize((104,42),Image.Resampling.BOX)
|
|
foil.putalpha(255)
|
|
pack("foil-v1.bin",[foil])
|
|
pack_city_wake.SRC = SRC
|
|
bodies = pack_city_wake.figures("coin-edge-v1.png",4)
|
|
scale = min(48/max(b.width for b in bodies),56/max(b.height for b in bodies))
|
|
frames = []
|
|
for body in bodies:
|
|
body = body.resize((round(body.width*scale),round(body.height*scale)),Image.Resampling.NEAREST)
|
|
bottom = body.height-1
|
|
xs = [x for x in range(body.width) if body.getpixel((x,bottom))[3]]
|
|
anchor = (min(xs)+max(xs))//2
|
|
frame = Image.new("RGBA",(64,64))
|
|
frame.paste(body,(32-anchor,60-bottom))
|
|
frames.append(frame)
|
|
pack("coin-edge-v1.bin",frames)
|
|
counter = opaque(Image.open(SRC / "counter-v1.png"))
|
|
counter = counter.crop(counter.getbbox())
|
|
scale = min(224/counter.width,240/counter.height)
|
|
counter = counter.resize((round(counter.width*scale),round(counter.height*scale)),Image.Resampling.NEAREST)
|
|
frame = Image.new("RGBA",(224,240))
|
|
frame.paste(counter,((224-counter.width)//2,240-counter.height))
|
|
pack("counter-v1.bin",[frame])
|
|
for index,ticket in enumerate(tickets):
|
|
ticket.save(ROOT / f"build/scratch/ticket-{index}-normalized.png")
|
|
sheet = Image.new("RGBA",(256,64))
|
|
for i,frame in enumerate(frames):
|
|
sheet.paste(frame,(64*i,0))
|
|
sheet.save(ROOT / "build/scratch/coin-normalized.png")
|
|
print("Packed lottery paper, four edge-contact coins (hotspot 32,60), grounded ticket counter")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|