"""Offline packer for the native MASM roulette sprite renderer.""" from pathlib import Path from collections import deque from PIL import Image ROOT = Path(__file__).resolve().parents[2] SIZE = 256 def cells(path): image = Image.open(path).convert("RGBA") for index in range(8): col, row = index % 4, index // 4 yield image.crop((round(col * image.width / 4), round(row * image.height / 2), round((col + 1) * image.width / 4), round((row + 1) * image.height / 2))) def main(): sprites = ROOT / "assets/sprites/roulette" frames = [] weapon_cells = list(cells(sprites / "revolver-actions-v1.png")) for frame_index, image in enumerate(weapon_cells): # Remove tiny disconnected atlas fragments, retaining muzzle flash components. pixels = bytearray(image.getchannel("A").point(lambda a: 255 if a >= 128 else 0).tobytes()) clean = bytearray(len(pixels)) components = [] for start, opacity in enumerate(pixels): if not opacity: continue component, queue = [], deque([start]) pixels[start] = 0 while queue: index = queue.popleft() component.append(index) x, y = index % image.width, index // image.width neighbors = [] if x: neighbors.append(index - 1) if x + 1 < image.width: neighbors.append(index + 1) if y: neighbors.append(index - image.width) if y + 1 < image.height: neighbors.append(index + image.width) for neighbor in neighbors: if pixels[neighbor]: pixels[neighbor] = 0 queue.append(neighbor) components.append(component) largest = max(map(len, components), default=0) for component in components: if len(component) == largest or (frame_index == 5 and len(component) >= largest * 0.07): for index in component: clean[index] = 255 image.putalpha(Image.frombytes("L", image.size, bytes(clean))) # Match the runtime alpha test so faint atlas noise cannot alter the anchor. bounds = image.getchannel("A").point(lambda a: 255 if a >= 128 else 0).getbbox() if bounds is None: raise ValueError("Empty revolver frame") image = image.crop(bounds) image.thumbnail((238, 238), Image.Resampling.LANCZOS) frame = Image.new("RGBA", (SIZE, SIZE)) frame.alpha_composite(image, ((SIZE - image.width) // 2, (SIZE - image.height) // 2)) frames.append(frame) for image in cells(sprites / "gman-talk-v1.png"): frame = image.resize((SIZE, SIZE), Image.Resampling.LANCZOS) frame.putalpha(255) frames.append(frame) target = ROOT / "assets/animations/roulette/roulette-scenes.bin" target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(b"".join(frame.tobytes("raw", "BGRA") for frame in frames)) assert target.stat().st_size == 16 * SIZE * SIZE * 4 contact = Image.new("RGBA", (4 * SIZE, 4 * SIZE), (20, 32, 30, 255)) for index, frame in enumerate(frames): contact.alpha_composite(frame, ((index % 4) * SIZE, (index // 4) * SIZE)) contact.convert("RGB").save(ROOT / "build/actions/roulette-contact.png") print(f"Packed {len(frames)} frames: {target}") if __name__ == "__main__": main()