- 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
94 lines
3.8 KiB
Python
94 lines
3.8 KiB
Python
"""Offline import of supplied artwork and exact interactive silver-area maps."""
|
|
from pathlib import Path
|
|
from collections import deque
|
|
import json
|
|
import struct
|
|
from PIL import Image, ImageDraw
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SRC = ROOT / "assets/sprites/scratch"
|
|
OUT = ROOT / "assets/animations/scratch"
|
|
SIZE = (540,720)
|
|
KINDS = (("neon-ru-v3.png",5,15),("ruby-ru-v3.png",6,18),("sea-ru-v3.png",4,12))
|
|
|
|
|
|
def silver_components(image):
|
|
w,h = image.size
|
|
pixels = image.load()
|
|
candidates = set()
|
|
for y in range(310,610):
|
|
for x in range(125,525):
|
|
r,g,b = pixels[x,y][:3]
|
|
if max(r,g,b)-min(r,g,b) < 22 and 105 < min(r,g,b) and max(r,g,b) < 225:
|
|
candidates.add((x,y))
|
|
components = []
|
|
while candidates:
|
|
start = candidates.pop()
|
|
queue = deque([start])
|
|
body = [start]
|
|
while queue:
|
|
x,y = queue.popleft()
|
|
for point in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)):
|
|
if point in candidates:
|
|
candidates.remove(point)
|
|
queue.append(point)
|
|
body.append(point)
|
|
if len(body)>1000:
|
|
components.append(body)
|
|
return sorted(components,key=lambda body:(min(y for x,y in body)//25,min(x for x,y in body)))
|
|
|
|
|
|
def pack(name,frames):
|
|
w,h = frames[0].size
|
|
raw = b"".join(image.tobytes("raw","BGRA") for image in frames)
|
|
(OUT/name).write_bytes(struct.pack("<4s4I",b"ANM1",w,h,len(frames),len(raw))+raw)
|
|
|
|
|
|
def main():
|
|
arts,maps,layouts = [],[],[]
|
|
for filename,winning,players in KINDS:
|
|
art = Image.open(SRC/filename).convert("RGBA").resize(SIZE,Image.Resampling.LANCZOS)
|
|
art.putalpha(255)
|
|
components = silver_components(art)
|
|
assert len(components)==winning+players,(filename,len(components))
|
|
field_map = Image.new("RGBA",SIZE)
|
|
fields = []
|
|
preview = art.copy()
|
|
draw = ImageDraw.Draw(preview)
|
|
for i,body in enumerate(components):
|
|
left,top = min(x for x,y in body),min(y for x,y in body)
|
|
right,bottom = max(x for x,y in body)+1,max(y for x,y in body)+1
|
|
assert right-left<=128 and bottom-top<=96
|
|
# Close tiny texture holes without expanding into the decorated frame.
|
|
rows = {}
|
|
for x,y in body:
|
|
rows.setdefault(y,[]).append(x)
|
|
area = 0
|
|
for y,xs in rows.items():
|
|
for x in range(min(xs),max(xs)+1):
|
|
field_map.putpixel((x,y),(0,0,i+1,255))
|
|
area += 1
|
|
fields.append(dict(x=left,y=top,w=right-left,h=bottom-top,area=area))
|
|
draw.rectangle((left,top,right-1,bottom-1),outline="red")
|
|
draw.text((left+4,top+4),str(i),fill="black")
|
|
preview.save(ROOT/f"build/scratch/{filename[:-4]}-fields.png")
|
|
arts.append(art)
|
|
maps.append(field_map)
|
|
layouts.append(dict(file=filename,winning=winning,players=players,fields=fields))
|
|
pack("portraits-v3.bin",arts)
|
|
pack("portrait-fields-v3.bin",maps)
|
|
(OUT/"portrait-layouts-v3.json").write_text(json.dumps(layouts,indent=2),encoding="utf-8")
|
|
lines = ["; Generated by pack_scratch_portraits.py; source coordinates, not screen coordinates.",".data"]
|
|
lines += ["scrPortraitWinning3D dd 5,6,4","scrPortraitPlayers3D dd 15,18,12","scrPortraitFields3D label DWORD"]
|
|
for layout in layouts:
|
|
fields = layout["fields"]
|
|
for field in fields:
|
|
lines.append(" dd "+",".join(str(field[k]) for k in ("x","y","w","h")))
|
|
lines.extend([" dd 0,0,0,0"]*(24-len(fields)))
|
|
lines += [".code",""]
|
|
(ROOT/"src_graphics/scratch_portrait_fields.inc").write_text("\n".join(lines),encoding="ascii")
|
|
print("Packed 3 supplied portrait designs, 20/24/16 exact silver masks and source-space geometry")
|
|
|
|
|
|
if __name__=="__main__":
|
|
main()
|