cd8d566d82
Порт PoP на Sprinter. Текущий PoC — applications/PoP/roomtest/: комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре и коллизией с картой. - roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип, seqtbl-анимация), pop_ctrl (порт control() PoP на held-state kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены, падение/приземление, отскок seq_47, вертикальный прыжок K4.1). MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge). - toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract — распаковка res-графики MSDOS в атласы + композиция комнат. - toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD), png_strip.py / room_compose.py (ассет-пайплайн). - docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT). - bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип). .gitignore: build-артефакты applications/*/*/*; исключены внешние референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
5.1 KiB
Python
136 lines
5.1 KiB
Python
#!/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("<HH", width, height))
|
|
f.write(bytes(canvas))
|
|
|
|
pal = bytearray()
|
|
if args.ega:
|
|
for b, g, r in EGA:
|
|
pal += bytes((b, g, r, 0))
|
|
for (r, g, b) in colors:
|
|
pal += bytes((b, g, r, 0))
|
|
open(args.out_pal, "wb").write(pal)
|
|
|
|
print(f"room_compose: {width}x{height} ({len(rows_imgs)} рядов) "
|
|
f"-> {args.out_raw} ({4 + width * height} Б, вкл. getimage-заголовок)")
|
|
print(f" палитра {len(colors)} своих цветов "
|
|
f"({len(pal)} Б) -> {args.out_pal}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|