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:
2026-07-17 17:51:08 +03:00
parent 484b18d10c
commit cd8d566d82
196 changed files with 6630 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
png_strip.py — генерик-конвертер произвольного набора PNG-кадров (РАЗНОГО
размера каждый) в одну raw-ленту + палитру для sprite-движка Sprinter
(формат getimage/atlas — см. libbgi/include/sprite.h, toolchain/mkatlas.py).
Не завязан ни на какой конкретный проект/игру — просто берёт список
картинок, которые вы сами укажете, и паковает их в ленту с общим
канвасом (кадр движка обязан быть фиксированного размера).
Использование:
png_strip.py OUT.raw OUT.pal img1.png img2.png ... [опции]
Опции:
--canvas WxH размер канваса кадра (по умолчанию — максимум
ширины/высоты среди входных картинок)
--anchor MODE bottom-center (по умолчанию) | top-left | center
--base-index N с какого индекса палитры начинать свои цвета
(по умолчанию 16 — под 16 EGA-слотов initgraph)
--ega добавить в палитру 16 EGA-цветов BGI перед своими
(по умолчанию включено; --no-ega выключает)
--seed-pal FILE переиспользовать цвета уже существующей палитры
(EGA16 + N своих) — свои индексы этой ленты
продолжаются ПОСЛЕ них (не с --base-index), а
OUT.pal получится ПОЛНОЙ merged-палитрой
(ega+seed+свои), готовой как --seed-pal для
следующего вызова. Так несколько независимо
собранных лент (напр. разные типы тайлов) можно
свести в ОДНУ палитру для одновременного показа
на экране (см. applications/PoP/poc/tools/
build_room_palette.py).
Каждая картинка — источник ОДНОГО кадра ленты (не атлас-сетка;
для сетки — см. mkatlas.py raw:FWxFH:NXxNY после этого шага, если
нужно). Прозрачность — альфа < 128 → индекс 0xFF (сентинел движка).
Дальше: toolchain/mkatlas.py OUT.atl OUT.raw:CW x CH:1xN
"""
import argparse
import sys
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"png_strip: {msg}")
def main():
ap = argparse.ArgumentParser(add_help=False)
ap.add_argument("out_raw")
ap.add_argument("out_pal")
ap.add_argument("images", nargs="+")
ap.add_argument("--canvas", default=None)
ap.add_argument("--anchor", default="bottom-center",
choices=["bottom-center", "top-left", "center"])
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")
ap.add_argument("--seed-pal", default=None)
args = ap.parse_args()
imgs = [Image.open(p).convert("RGBA") for p in args.images]
seed_records = []
if args.seed_pal:
raw = open(args.seed_pal, "rb").read()
if len(raw) % 4:
die(f"--seed-pal {args.seed_pal}: длина не кратна 4")
n_ega = 16 if args.ega else 0
for i in range(n_ega, len(raw) // 4):
b, g, r, _ = raw[i * 4:i * 4 + 4]
seed_records.append((r, g, b))
args.base_index = 16 + len(seed_records)
if args.canvas:
cw, ch = (int(v) for v in args.canvas.lower().split("x"))
else:
cw = max(im.width for im in imgs)
ch = max(im.height for im in imgs)
if cw > 64 or ch > 64:
die(f"канвас {cw}x{ch} превышает лимит движка 64x64")
for p, im in zip(args.images, imgs):
if im.width > cw or im.height > ch:
die(f"{p}: {im.width}x{im.height} не влезает в канвас {cw}x{ch}")
colors = {}
for i, rgb in enumerate(seed_records):
colors[rgb] = 16 + i
def color_index(r, g, b):
key = (r, g, b)
if key not in colors:
idx = args.base_index + (len(colors) - len(seed_records))
if idx >= 0xFF:
die("палитра переполнена (>= 0xFF своих цветов)")
colors[key] = idx
return colors[key]
data = bytearray()
for im in imgs:
if args.anchor == "top-left":
ox, oy = 0, 0
elif args.anchor == "center":
ox, oy = (cw - im.width) // 2, (ch - im.height) // 2
else: # bottom-center
ox, oy = (cw - im.width) // 2, ch - im.height
canvas = [0xFF] * (cw * ch)
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[(oy + y) * cw + (ox + x)] = color_index(r, g, b)
data.extend(canvas)
open(args.out_raw, "wb").write(data)
pal = bytearray()
if args.ega:
for b, g, r in EGA:
pal += bytes((b, g, r, 0))
new_colors = [rgb for rgb in colors if rgb not in seed_records]
for (r, g, b) in (*seed_records, *new_colors):
pal += bytes((b, g, r, 0))
open(args.out_pal, "wb").write(pal)
print(f"png_strip: {len(imgs)} кадров, канвас {cw}x{ch}, "
f"{len(data)} Б -> {args.out_raw}")
print(f" палитра {len(seed_records)} seed + {len(new_colors)} новых "
f"своих цветов ({len(pal)} Б) -> {args.out_pal}")
print(f"дальше: mkatlas.py OUT.atl {args.out_raw}:{cw}x{ch}:1x{len(imgs)}")
if __name__ == "__main__":
main()