Files
Sprinter-SDCC/applications/PoP/toolchain/pop_pack_guard.py
T
snark13 6673279cef PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
  poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
  curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
  УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
  стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
  только в кадрах 150..189.  Пока ветка была одна (charid_2_guard), скелет
  получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
  комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.

Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
  guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
  Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
  меняется вместе со стражем.  Грабля: gfx_pal_load отдаёт указатель в BIOS,
  а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.

Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
  Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
  после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
  обратно.  +48 байт W2.

Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
  seg008:1B06) и расширение перебора объединённым прямоугольником
  «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
  кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
  (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
  «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
  вернувшейся кучей костей.

Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.

Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).

Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).

В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:11:03 +03:00

197 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
pop_pack_guard.py — упаковщик спрайтов СТРАЖА (chtab_5_guard, база 750).
Оригинал грузит стражей из отдельного .DAT по типу уровня
(GUARD/FAT/SKEL/VIZIER/SHADOW, seg000:1092); уровень 1 — обычный «guard»,
то есть data/GUARD/res(750+id).png. Палитра — НЕ из PNG: оригинал
подменяет её из res10.bin (guard_palettes, 7 палитр по 16 цветов, 6-бит) по
level.guards_color; на уровне 1 у обоих стражей color = 2.
Выход: poc/res/guard/g0..gN.atl (по 8 спрайтов на страницу, адресация
id>>3 / id&7 — как у Kid) + группа палитры 0x90..0x9F в общий kid.pal
(его собирает pop_pack_kid.py, см. GUARD_PAL_BASE там).
"""
import os
import sys
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pop_pack_bg as B
from pop_pack_kid import transpose_cols
# Набор спрайтов соперника выбирается ТИПОМ УРОВНЯ (load_lev_spr, seg000:1092):
# tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, 1 нет}.
# У обычного стража палитра подменяется из res10.bin по guards_color, у
# остальных наборов curr_guard_color = 0 (seg002:186) — значит цвет берётся
# из ФАЙЛА набора (res750.pal), и подменять его не надо.
SETS = {
"GUARD": ("GUARD", "guard", None),
"SKEL": ("SKEL", "skel", "res750.pal"),
}
SET_NAME = "GUARD"
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD")
PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin")
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard")
PAL_BASE = 0x90 # слоты палитры Sprinter под стража
COLOR = 2 # level.guards_color обоих стражей уровня 1
SHIFT = 3 # g[id>>3], idx id&7 (спрайты крупные)
MASK = (1 << SHIFT) - 1
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
def select_set(name):
"""Переключить упаковщик на набор (GUARD/SKEL): каталоги и число кадров."""
global SET_NAME, GUARD_DIR, OUT_DIR, MAX_ID
if name not in SETS:
B.die("неизвестный набор %s (есть: %s)" % (name, ", ".join(SETS)))
SET_NAME = name
src, out, _ = SETS[name]
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", src)
OUT_DIR = os.path.join(HERE, "..", "poc", "res", out)
# Сколько кадров реально есть в наборе (у скелета их меньше, чем у стража).
n = 0
while os.path.exists(os.path.join(GUARD_DIR, f"res{751 + n}.png")):
n += 1
MAX_ID = n - 1
def set_palette():
"""16 цветов набора из его res750.pal (для наборов, где цвет НЕ
подменяется из res10.bin). None — у набора своей палитры нет."""
fname = SETS[SET_NAME][2]
if fname is None:
return None
b = open(os.path.join(GUARD_DIR, fname), "rb").read()
return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]
def load_guard(id):
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
Off-by-one как у Kid (см. pop_pack_kid.load_kid): load_chtab_from_file
зовётся с ресурсом 750, а images[i-1] = res(750+i), то есть images[0] =
res751. Рисование берёт get_image(chtab, obj_id+1 1) = images[image],
значит frame.image=N -> res(751+N). С res(750+N) страж стоял в позе
предыдущего кадра (замечено на frame_166_stand_inactive: image 17
рисовался как res767 — выпад — вместо res768). """
path = os.path.join(GUARD_DIR, f"res{751 + id}.png")
if not os.path.exists(path):
return None
im = Image.open(path)
if im.mode != "P":
B.die(f"res{750+id}: mode {im.mode}, ожидался P")
src = im.tobytes()
rowmajor = bytes((0xFF if v == 0 else PAL_BASE + v) for v in src)
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
NPAL = 7 # guard_palettes: 7 палитр по 16 цветов (res10.bin)
def guard_palette(color=None):
"""16 цветов (r,g,b 0..63) из res10.bin для палитры color (1..7).
Индексация как в оригинале (seg003:257): &guard_palettes[0x30*color-0x30].
color=None — палитра COLOR (та, что запекается в kid.pal по умолчанию)."""
if color is None:
color = COLOR
b = open(PAL_BIN, "rb").read()
off = 0x30 * color - 0x30
return [(b[off + i * 3], b[off + i * 3 + 1], b[off + i * 3 + 2]) for i in range(16)]
def write_pal_header(out_path):
"""Таблица ВСЕХ палитр стражей -> заголовок для приложения.
Оригинал держит спрайты стража в одной палитре, а цвет подменяет на лету:
redraw_screen (seg003:255) зовёт set_chtab_palette(chtab_5_guard,
&guard_palettes[0x30*curr_guard_color - 0x30], 16) ПЕРЕД отрисовкой
комнаты, где curr_guard_color = level.guards_color[room-1] & 0x0F
(enter_guard, seg002:184). У нас спрайты уже лежат в индексах
PAL_BASE + v, поэтому достаточно залить 16 слотов палитры.
Формат записи — родной для gfx_pal_load: 4 байта (B, G, R, 0)."""
rows = []
for color in range(1, NPAL + 1):
vals = []
for (r, g, b) in guard_palette(color):
# res10.bin хранит 6-битные каналы, палитра Sprinter — 8-битные
# (так же делает pop_pack_kid при записи kid.pal).
vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0]
rows.append(" { " + ",".join("0x%02X" % v for v in vals) + " },")
lines = [
"/* pop_guard_pal.h — палитры стражей (guard_palettes из res10.bin).",
" * СГЕНЕРЕНО toolchain/pop_pack_guard.py — НЕ править вручную.",
" *",
" * Индексация как в оригинале: цвет из level.guards_color[room-1]",
" * (младшие 4 бита), 1..%d -> строка [color-1]; 0 = палитру НЕ менять" % NPAL,
" * (seg003:255 при curr_guard_color == 0 set_chtab_palette не зовёт).",
" * Запись — 4 байта (B, G, R, 0), формат gfx_pal_load. */",
"#ifndef POP_GUARD_PAL_H",
"#define POP_GUARD_PAL_H",
"",
"#include <stdint.h>",
"",
"#define POP_GUARD_PAL_BASE 0x%02X /* слоты Sprinter под chtab_5 */" % PAL_BASE,
"#define POP_GUARD_NPAL %d" % NPAL,
"",
"static const uint8_t pop_guard_pal[POP_GUARD_NPAL][64] = {",
] + rows + [
"};",
"",
]
# Палитра скелета — из ЕГО набора (res750.pal), а не из res10.bin: на
# уровне 3 curr_guard_color = 0, оригинал палитру не подменяет вовсе.
saved = SET_NAME
select_set("SKEL")
skel = set_palette()
select_set(saved)
if skel:
vals = []
for (r, g, b) in skel:
vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0]
lines += [
"/* Скелет (SKEL/res750.pal): свой набор — свой цвет. */",
"static const uint8_t pop_skel_pal[64] = {",
" " + ",".join("0x%02X" % v for v in vals) + ",",
"};",
"",
]
lines += [
"#endif",
"",
]
with open(out_path, "w") as f:
f.write("\n".join(lines))
print(f" pop_guard_pal.h: {NPAL} палитр x 16 цветов = {NPAL*64} Б -> {out_path}")
def main():
os.makedirs(OUT_DIR, exist_ok=True)
pages = {}
n = 0
for id in range(0, MAX_ID + 1):
g = load_guard(id)
if g is None:
continue
pages.setdefault(id >> SHIFT, {})[id & MASK] = g
n += 1
if not pages:
B.die("нет спрайтов стража")
total = 0
for pg in range(max(pages) + 1):
nm = f"g{pg}.atl"
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
total += sz
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б")
write_pal_header(os.path.join(HERE, "..", "roomtest", "pop_guard_pal.h"))
print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
if __name__ == "__main__":
if len(sys.argv) > 1:
select_set(sys.argv[1].upper())
main()