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>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user