Files
snark13 cd8d566d82 applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн
Порт 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>
2026-07-17 17:51:08 +03:00

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()