#!/usr/bin/env python3 """ room_compose.py — генерик-компоновщик полноэкранного фона комнаты из тайлов ПРОИЗВОЛЬНОГО размера (см. applications/PoP/docs/PORT_PLAN.md §4: один полноэкранный фон на комнату готовится офлайн, поверх — спрайты через sprite.h). Не завязан ни на какой конкретный проект/игру — просто берёт раскладку тайлов, которую вы сами опишете, и картинки, которые сами укажете. Раскладка — JSON: { "rows": [ {"height": 20, "tiles": ["a.png", "b.png", ...]}, {"height": 60, "tiles": [...]}, ... ] } Тайлы в ряду кладутся слева направо; высота ряда — из "height" (тайл меньшей высоты — сверху, остаток — прозрачность 0xFF; выше — ошибка). Ширина итоговой картинки = сумма ширин тайлов первого ряда — остальные ряды обязаны давать ТУ ЖЕ сумму (иначе ошибка, во избежание кривого экрана по краю). Использование: room_compose.py OUT.raw OUT.pal --layout layout.json [--base-index N] Выход: OUT.raw — ГОТОВЫЙ формат getimage (uint16 w, uint16 h LE + пиксели построчно) — грузится fread'ом целиком и передаётся напрямую в gfx_blit(x, y, buf). OUT.pal — палитра (EGA 16 + свои цвета), формат gfx_pal_fload. """ import argparse import json import struct import sys from pathlib import Path from PIL import Image EGA = [ (0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0), (0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168), (84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84), (84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255), ] def die(msg): sys.exit(f"room_compose: {msg}") def main(): ap = argparse.ArgumentParser(add_help=False) ap.add_argument("out_raw") ap.add_argument("out_pal") ap.add_argument("--layout", required=True) ap.add_argument("--base-index", type=int, default=16) ap.add_argument("--ega", dest="ega", action="store_true", default=True) ap.add_argument("--no-ega", dest="ega", action="store_false") args = ap.parse_args() spec = json.loads(Path(args.layout).read_text()) rows_spec = spec["rows"] if not rows_spec: die("layout: пустой список rows") rows_imgs = [] width = None for ri, row in enumerate(rows_spec): rh = int(row["height"]) tiles = row["tiles"] if not tiles: die(f"row {ri}: пустой список tiles") imgs = [Image.open(t).convert("RGBA") for t in tiles] for t, im in zip(tiles, imgs): if im.height > rh: die(f"{t}: высота {im.height} > высоты ряда {rh}") row_w = sum(im.width for im in imgs) if width is None: width = row_w elif row_w != width: die(f"row {ri}: ширина {row_w} != ширины первого ряда {width} " f"(все ряды должны давать одинаковую суммарную ширину)") rows_imgs.append((rh, imgs)) height = sum(rh for rh, _ in rows_imgs) if width > 65535 or height > 65535: die(f"{width}x{height}: превышает предел формата (u16)") colors = {} def color_index(r, g, b): key = (r, g, b) if key not in colors: idx = args.base_index + len(colors) if idx >= 0xFF: die("палитра переполнена (>= 0xFF своих цветов)") colors[key] = idx return colors[key] canvas = [0xFF] * (width * height) y0 = 0 for rh, imgs in rows_imgs: x0 = 0 for im in imgs: for y in range(im.height): for x in range(im.width): r, g, b, a = im.getpixel((x, y)) if a < 128: continue canvas[(y0 + y) * width + (x0 + x)] = color_index(r, g, b) x0 += im.width y0 += rh with open(args.out_raw, "wb") as f: f.write(struct.pack(" {args.out_raw} ({4 + width * height} Б, вкл. getimage-заголовок)") print(f" палитра {len(colors)} своих цветов " f"({len(pal)} Б) -> {args.out_pal}") if __name__ == "__main__": main()