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:
@@ -43,7 +43,8 @@ OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg")
|
||||
|
||||
# Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты
|
||||
# столбов, ворот, дебриса. См. tile_table.fore_id + анализ pop_background_strategy.
|
||||
FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100}
|
||||
FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100} | {106, 107, 108, 109, 110} \
|
||||
| {119, 120, 121, 122, 123} # фронт чомпера + его кровь
|
||||
|
||||
# Пол-оверлей подъёма (floor_left_overlay, draw_floor_overlay seg008): рисуется
|
||||
# рантаймом поверх нижней части Kid при подтягивании на пол у кромки — НЕ
|
||||
@@ -65,6 +66,23 @@ SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left
|
||||
134, 135, 136, 137, 138, # right
|
||||
139, 140, 141, 142, 143} # fore
|
||||
|
||||
# Кадры ЧОМПЕРА (seg008:638 chomper_fram_bot/top/for + кровь). В уровнях
|
||||
# чомпер стоит с modif = 0, а chomper_fram1[0] = 3 — значит render_room
|
||||
# (он анимацию пропускает вовсе, см. render_room.py:611) не кладёт в атлас
|
||||
# НИ ОДНОГО кадра, и чомпер не виден даже в покое. Поэтому весь набор —
|
||||
# явно. Низ и верх идут в backtable (ПОД персонажем), фронт — в foretable
|
||||
# (ПОВЕРХ), поэтому фронтовые id продублированы в FORE_ENV_IDS.
|
||||
CHOMPER_BOT_IDS = {101, 102, 103, 104, 105} # chomper_fram_bot
|
||||
CHOMPER_TOP_IDS = {111, 112, 113} # chomper_fram_top (фазы 2..4)
|
||||
CHOMPER_FORE_IDS = {106, 107, 108, 109, 110} # chomper_fram_for
|
||||
|
||||
# Кровь на сомкнутых челюстях (modif & 0x80): chomper_num + 114 в backtable и
|
||||
# + 119 в foretable, обе — MONO-блитом цветом 12 (blitters_4Ch_mono_12), то
|
||||
# есть силуэтом, как пузырёк зелья. Появляется после смерти Кида в чомпере.
|
||||
CHOMPER_BLOOD_IDS = {114, 115, 116, 117, 118}
|
||||
CHOMPER_BLOOD_FORE_IDS = {119, 120, 121, 122, 123}
|
||||
CHOMPER_BLOOD_COLOR = None # ставится ниже, когда известен VGA16_PAL_BASE
|
||||
|
||||
# Кадры ОТКРЫТИЯ ворот (portcullis, DOOR_FRAM_SLICE + gate pieces 50/51,
|
||||
# seg008 draw_gate): 50-59 + 67. В уровнях ворота статически ЗАКРЫТЫ
|
||||
# (низкий modif → используются только нижние срезы 50-53); срезы более
|
||||
@@ -105,6 +123,7 @@ VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(42,21,0),
|
||||
(63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal
|
||||
POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета)
|
||||
POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение»)
|
||||
CHOMPER_BLOOD_COLOR = VGA16_PAL_BASE + 12 # тот же mono-цвет 12 (seg008:678)
|
||||
|
||||
# Основание факела (seg008:489 draw_tile_right: при левом соседе-факеле
|
||||
# рисуется chtab_6 id 146). В статический render_room не попадает.
|
||||
@@ -174,7 +193,7 @@ LEVELDOOR_ENV_IDS = {33, 34, 99, 144}
|
||||
NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149} | LEVELDOOR_ENV_IDS
|
||||
|
||||
|
||||
def load_indexed(chtab, id, opaque=False):
|
||||
def load_indexed(chtab, id, opaque=False, mono=None):
|
||||
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
|
||||
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).
|
||||
|
||||
@@ -195,7 +214,11 @@ def load_indexed(chtab, id, opaque=False):
|
||||
if im.mode != "P":
|
||||
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
|
||||
src = im.tobytes()
|
||||
if opaque:
|
||||
if mono is not None:
|
||||
# MONO-блит (blitters_4Ch_mono_12): спрайт красится ОДНИМ цветом из
|
||||
# 16-цветной палитры экрана — пакуем силуэтом, как пузырёк зелья.
|
||||
out = bytes((0xFF if v == 0 else mono) for v in src)
|
||||
elif opaque:
|
||||
out = bytes((pal_base + v) for v in src)
|
||||
else:
|
||||
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
|
||||
@@ -363,6 +386,11 @@ def main():
|
||||
| GATE_ANIM_ENV_IDS # + кадры открытия ворот
|
||||
| TORCH_ENV_IDS # + основание факела
|
||||
| STUCK_ENV_IDS # + нажатая кнопка-closer
|
||||
| CHOMPER_BOT_IDS # + чомпер: низ
|
||||
| CHOMPER_TOP_IDS # верх
|
||||
| CHOMPER_FORE_IDS # фронт (поверх Kid)
|
||||
| CHOMPER_BLOOD_IDS # кровь (mono)
|
||||
| CHOMPER_BLOOD_FORE_IDS
|
||||
| OPENER_NOFLOOR_ENV_IDS # + opener без пола слева
|
||||
| LEVELDOOR_ENV_IDS) # + дверь уровня (створка/лестница)
|
||||
wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL)
|
||||
@@ -373,7 +401,9 @@ def main():
|
||||
# ENV фон -> страницы по id>>SHIFT
|
||||
pages = {}
|
||||
for id in env_bg:
|
||||
w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS)
|
||||
w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS,
|
||||
CHOMPER_BLOOD_COLOR if id in CHOMPER_BLOOD_IDS
|
||||
else None)
|
||||
pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
|
||||
max_pg = max(pages) if pages else -1
|
||||
env_page_names = []
|
||||
@@ -396,7 +426,10 @@ def main():
|
||||
print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
# FORE -> idx=id
|
||||
fore_entries = {id: load_indexed(R.CHTAB_ENV, id) for id in env_fore}
|
||||
fore_entries = {id: load_indexed(R.CHTAB_ENV, id, False,
|
||||
CHOMPER_BLOOD_COLOR
|
||||
if id in CHOMPER_BLOOD_FORE_IDS else None)
|
||||
for id in env_fore}
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_fore.atl"), fore_entries)
|
||||
total_bytes += sz
|
||||
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user