#!/usr/bin/env python3 """Package imagegen sources into palette SD/HD game footprints and synthesize WAVs. This performs deterministic cropping/resampling/palette conversion, not image generation. The three imagegen originals remain beside the packaged local files. """ import argparse import hashlib import json import math import random import struct import wave from pathlib import Path import runpy from PIL import Image, ImageOps ROOT = Path(__file__).resolve().parent def palette_image(path): raw = path.read_bytes() if len(raw) != 768: raise ValueError("expected the original 768-byte VGA palette") colors = [(min(value, 63) << 2) | (min(value, 63) >> 4) for value in raw] palette = Image.new("P", (1, 1)) palette.putpalette(colors) return palette, colors def sd(image, size, palette): image = image.convert("RGBA").resize(size, Image.Resampling.LANCZOS) alpha = image.getchannel("A").point(lambda a: 255 if a >= 128 else 0) # Use one full-canvas palette pass before splitting terrain, so tile borders # receive identical context and no independent quantizer creates seams. rgb = image.convert("RGB").quantize(palette=palette, dither=Image.Dither.NONE).convert("RGBA") rgb.putalpha(alpha) return rgb def wav(name, duration, notes, mode="tone"): sample_rate = 22050 rng = random.Random(7053 + sum(map(ord, name))) values = bytearray() count = int(duration * sample_rate) for frame in range(count): t = frame / sample_rate section = min(len(notes) - 1, int(t / duration * len(notes))) frequency = notes[section] local = (t / duration * len(notes)) % 1 envelope = min(1, local * 18) * (1 - local) ** 0.8 signal = math.sin(2 * math.pi * frequency * t) signal += 0.24 * math.sin(2 * math.pi * frequency * 2 * t) if mode == "metal": signal += 0.45 * rng.uniform(-1, 1) * math.exp(-t * 15) signal += 0.3 * math.sin(2 * math.pi * 817 * t) * math.exp(-t * 10) if mode == "alarm": signal += 0.25 * math.sin(2 * math.pi * frequency * 0.5 * t) value = max(-32767, min(32767, int(signal * envelope * 7200))) values += struct.pack("