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>
This commit is contained in:
2026-08-07 22:11:03 +03:00
parent 18d177407b
commit 6673279cef
55 changed files with 3340 additions and 1304 deletions
+115 -3
View File
@@ -21,6 +21,16 @@ 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")
@@ -31,6 +41,32 @@ 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 (прозрачность).
@@ -52,13 +88,86 @@ def load_guard(id):
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
def guard_palette():
"""16 цветов (r,g,b 0..63) из res10.bin для COLOR."""
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
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 = {}
@@ -77,8 +186,11 @@ def main():
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()