Уровень 4: второй тайлсет (дворец) — ассеты и переключение (шаги A и B)
Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один и тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE. Упаковщик (toolchain/pop_pack_bg.py): - аргумент набора: `pop_pack_bg.py dungeon|palace`. Каскад каталогов — сначала свой набор, потом чужой фолбэком (в распакованном data/ res230/ 231/348 есть только в VDUNGEON, два десятка — только в VPALACE). - ОБА набора пакуются по одному объединению id, поэтому раскладка id -> (страница, idx) общая и заголовок один: коду достаточно подменить имена файлов. - PALACE_ENV_IDS: 78/80/82 (doortop_fram_bot), 81/83 (doortop_fram_top), 84 (полоска стены), 145 (stripe_id) — их рисует только палас, render_room про них не знает. - ENV_SHIFT 5 -> 4: с паласными кусками страница 2 переваливала за 16 КБ (16 996). Цена — 10 страниц EMM на набор вместо 5, при 215 свободных. - *tile.pal: 32 записи (env 0x50..0x5F + wall 0x60..0x6F) на набор. Полная kid.pal не трогается — Кид, страж, меч и зелья в других слотах. Движок: - pop_level_type() (tbl_level_type, SDLPoP data.h:840): дворцовые уровни 4, 5, 6, 10, 11, 14. Живёт в pop_level.c, потому что по типу расходятся не только атласы, но и ветки отрисовки seg008, кладка стены и модификатор пустой клетки от упавшей плиты (remove_loose, seg007:0EB8). - pop_bg_load(set): no-op при том же наборе, при смене выгружает старый (иначе текут 12 EMM-страниц) и правит 32 записи палитры в ОБЕ страницы дабл-буфера. Зовётся на старте и на границе уровня, не в кадре. - Путь к атласу склеивается на месте (bg_path): двадцать строк-имён в банке — лишние полкилобайта. Проверено в MAME: уровень 1 (подземелье) рисуется как прежде; уровень 4 (-DFIRST_LEVEL=4) — дворцовые арки, окна, пол, решётчатая дверь уровня, Кид не перекрашен. Стены пока чёрные: паласный wall_pattern — шаг C. tests-host: 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,37 @@ CHOMPER_BLOOD_COLOR = VGA16_PAL_BASE + 12 # тот же mono-цвет 12 (seg0
|
||||
# рисуется chtab_6 id 146). В статический render_room не попадает.
|
||||
TORCH_ENV_IDS = {146}
|
||||
|
||||
ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31
|
||||
# ---- Два тайлсета: подземелье и дворец --------------------------------
|
||||
# Оригинал грузит РАЗНЫЙ .DAT под один и тот же набор id:
|
||||
# tbl_envir_ki[tbl_level_type[level]] -> VDUNGEON / VPALACE (seg000:1108),
|
||||
# то есть id спрайта от набора НЕ зависит — меняется только картинка и
|
||||
# палитра (env 0x50..0x5F, wall 0x60..0x6F). Поэтому оба набора пакуются
|
||||
# по ОДНОМУ И ТОМУ ЖЕ объединению id: раскладка id -> (страница, idx)
|
||||
# остаётся одна, и коду достаточно подменить ИМЕНА файлов.
|
||||
#
|
||||
# Порядок каталогов — каскад: сначала «свой» набор, потом чужой как
|
||||
# фолбэк (в распакованном data/ несколько id есть только в одном из них:
|
||||
# res230/231/348 только в VDUNGEON, два десятка — только в VPALACE).
|
||||
SETS = {
|
||||
"dungeon": ("pop_", lambda: (R.VDUNGEON_DIR, R.VPALACE_DIR)),
|
||||
"palace": ("pal_", lambda: (R.VPALACE_DIR, R.VDUNGEON_DIR)),
|
||||
}
|
||||
SRC_DIRS = None # ставит main() по выбранному набору
|
||||
|
||||
# Спрайты, которые рисуются ТОЛЬКО на паласных уровнях (seg008):
|
||||
# 78, 80, 82 doortop_fram_bot (низ верха двери, draw_tile_right)
|
||||
# 81, 83 doortop_fram_top (draw_tile_topright)
|
||||
# 84 полоска на стене (draw_tile_right, tiles_20_wall)
|
||||
# 145 stripe_id соседнего тайла (draw_tile_right)
|
||||
# render_room про них не знает (он рисует по подземельной ветке), поэтому
|
||||
# добавляем явно — иначе при переключении набора в этих местах будет дыра.
|
||||
PALACE_ENV_IDS = {78, 80, 81, 82, 83, 84, 145}
|
||||
|
||||
ENV_SHIFT = 4 # env фон: страница = id>>SHIFT, idx = id&MASK.
|
||||
# Было 5: паласный набор добавляет куски верха
|
||||
# двери (78..84), и страница 2 переваливала за
|
||||
# 16 КБ. При 4 обе раскладки укладываются, цена
|
||||
# — 10 страниц EMM на набор вместо 5.
|
||||
ENV_MASK = (1 << ENV_SHIFT) - 1
|
||||
|
||||
# ---- .atl формат (== libbgi, но каталог не ограничен 19) --------------
|
||||
@@ -194,7 +224,8 @@ NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149} | LEVELDOOR_ENV_IDS
|
||||
|
||||
|
||||
def load_indexed(chtab, id, opaque=False, mono=None):
|
||||
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
|
||||
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад по SRC_DIRS
|
||||
(порядок задаёт выбранный тайлсет, см. SETS).
|
||||
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).
|
||||
|
||||
opaque=True — пиксель 0 остаётся ЦВЕТОМ (base+0 = чёрный), а не ключом
|
||||
@@ -203,7 +234,7 @@ def load_indexed(chtab, id, opaque=False, mono=None):
|
||||
pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50
|
||||
fname = f"res{base_res + id}.png"
|
||||
path = None
|
||||
for d in (R.VDUNGEON_DIR, R.VPALACE_DIR):
|
||||
for d in SRC_DIRS:
|
||||
p = os.path.join(d, fname)
|
||||
if os.path.exists(p):
|
||||
path = p
|
||||
@@ -306,11 +337,29 @@ def scale6to8(v):
|
||||
return (v * 255) // 63
|
||||
|
||||
|
||||
def read_pal16(res, d):
|
||||
b = open(os.path.join(d, res), "rb").read()
|
||||
return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]
|
||||
|
||||
|
||||
def build_tileset_palette(out_path):
|
||||
"""Палитра ТОЛЬКО тайлсета: 32 записи B,G,R,0 — env (16) затем wall (16).
|
||||
Ею подменяются слоты 0x50..0x6F при смене типа уровня; полная игровая
|
||||
палитра (kid.pal) при этом не трогается, потому что Кид, страж, меч и
|
||||
зелья живут в других слотах (levels_plan §3). Порядок каталогов —
|
||||
SRC_DIRS, то есть файл соответствует выбранному набору."""
|
||||
pal = bytearray(32 * 4)
|
||||
src = read_pal16("res200.pal", SRC_DIRS[0]) + read_pal16("res360.pal", SRC_DIRS[0])
|
||||
for i, (r, g, b) in enumerate(src):
|
||||
pal[i * 4:i * 4 + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(pal)
|
||||
|
||||
|
||||
def build_palette(out_path):
|
||||
"""256-записная палитра B,G,R,0: env@0x50..0x5F, wall@0x60..0x6F."""
|
||||
def read_pal(res, d):
|
||||
b = open(os.path.join(d, res), "rb").read()
|
||||
return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]
|
||||
return read_pal16(res, d)
|
||||
|
||||
env = read_pal("res200.pal", R.VDUNGEON_DIR)
|
||||
wall = read_pal("res360.pal", R.VDUNGEON_DIR)
|
||||
@@ -334,7 +383,10 @@ def build_palette(out_path):
|
||||
|
||||
|
||||
# ---- 5. C-заголовок --------------------------------------------------
|
||||
def write_header(out_path, env_pages, wall_name, fore_name):
|
||||
def write_header(out_path, env_pages):
|
||||
"""Заголовок ОДИН на оба тайлсета: раскладка id -> (страница, idx) у них
|
||||
общая (пакуются по одному объединению id), различаются только имена
|
||||
файлов и палитра. Поэтому здесь два массива имён и один индекс."""
|
||||
lines = [
|
||||
"/* pop_bg_atlas.h — раскладка атласов статического фона PoP.",
|
||||
" * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.",
|
||||
@@ -343,6 +395,11 @@ def write_header(out_path, env_pages, wall_name, fore_name):
|
||||
" * ENV фон id N -> atlas env_bg[N>>%d], idx N&%d" % (ENV_SHIFT, ENV_MASK),
|
||||
" * WALL id N -> atlas wall, idx N",
|
||||
" * FORE id N -> atlas fore, idx N",
|
||||
" *",
|
||||
" * ДВА ТАЙЛСЕТА (порт tbl_envir_ki[tbl_level_type], seg000:1108):",
|
||||
" * 0 = подземелье (pop_*), 1 = дворец (pal_*). Набор id и раскладка",
|
||||
" * у них ОДНИ И ТЕ ЖЕ — меняются только файлы и 32 записи палитры",
|
||||
" * (env 0x50..0x5F, wall 0x60..0x6F), см. *tile.pal.",
|
||||
" */",
|
||||
"#ifndef POP_BG_ATLAS_H",
|
||||
"#define POP_BG_ATLAS_H",
|
||||
@@ -350,22 +407,32 @@ def write_header(out_path, env_pages, wall_name, fore_name):
|
||||
"#define POP_ENV_SHIFT %d" % ENV_SHIFT,
|
||||
"#define POP_ENV_MASK %d" % ENV_MASK,
|
||||
"#define POP_ENV_PAGES %d" % len(env_pages),
|
||||
"#define POP_TILESETS 2",
|
||||
"",
|
||||
"/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */",
|
||||
"/* Палитра: env-слоты, wall-слоты (спрайт-пиксель i -> база+i). */",
|
||||
"#define POP_PAL_ENV 0x50",
|
||||
"#define POP_PAL_WALL 0x60",
|
||||
"",
|
||||
"/* Имена файлов атласов (грузятся atlas_load). */",
|
||||
"static const char *const pop_env_atl[POP_ENV_PAGES] = {",
|
||||
"/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы",
|
||||
" * видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия",
|
||||
" * строк уедет в каждый включивший заголовок модуль. */",
|
||||
"#ifdef POP_BG_ATLAS_NAMES",
|
||||
"static const char *const pop_env_atl[POP_TILESETS][POP_ENV_PAGES] = {",
|
||||
]
|
||||
for pg in range(len(env_pages)):
|
||||
nm = env_pages[pg]
|
||||
lines.append(' %s,' % ('"%s"' % nm if nm else "0 /* пусто */"))
|
||||
for prefix, _ in SETS.values():
|
||||
row = ", ".join(('"%s%s"' % (prefix, nm.split("_", 1)[1]) if nm else "0")
|
||||
for nm in env_pages)
|
||||
lines.append(" { %s }," % row)
|
||||
lines += [
|
||||
"};",
|
||||
'#define POP_WALL_ATL "%s"' % wall_name,
|
||||
'#define POP_FORE_ATL "%s"' % fore_name,
|
||||
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */',
|
||||
'static const char *const pop_wall_atl[POP_TILESETS] = { %s };'
|
||||
% ", ".join('"%swall.atl"' % p for p, _ in SETS.values()),
|
||||
'static const char *const pop_fore_atl[POP_TILESETS] = { %s };'
|
||||
% ", ".join('"%sfore.atl"' % p for p, _ in SETS.values()),
|
||||
'static const char *const pop_tile_pal[POP_TILESETS] = { %s };'
|
||||
% ", ".join('"%stile.pal"' % p for p, _ in SETS.values()),
|
||||
"#endif /* POP_BG_ATLAS_NAMES */",
|
||||
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */',
|
||||
"#define POP_PAL_POT 0x%02X" % POT_PAL_BASE,
|
||||
'#define POP_BG_PAL "pop_bg.pal"',
|
||||
"",
|
||||
@@ -377,6 +444,14 @@ def write_header(out_path, env_pages, wall_name, fore_name):
|
||||
|
||||
|
||||
def main():
|
||||
global SRC_DIRS
|
||||
set_name = sys.argv[1] if len(sys.argv) > 1 else "dungeon"
|
||||
if set_name not in SETS:
|
||||
die(f"набор '{set_name}': ожидался один из {', '.join(SETS)}")
|
||||
prefix, dirs = SETS[set_name]
|
||||
SRC_DIRS = dirs()
|
||||
print(f"набор: {set_name} (источник {os.path.basename(SRC_DIRS[0])}, "
|
||||
f"фолбэк {os.path.basename(SRC_DIRS[1])}), префикс {prefix}")
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
used = collect_used()
|
||||
env_ids = sorted(set(id for ch, id in used if ch != R.CHTAB_WALL)
|
||||
@@ -392,7 +467,8 @@ def main():
|
||||
| CHOMPER_BLOOD_IDS # кровь (mono)
|
||||
| CHOMPER_BLOOD_FORE_IDS
|
||||
| OPENER_NOFLOOR_ENV_IDS # + opener без пола слева
|
||||
| LEVELDOOR_ENV_IDS) # + дверь уровня (створка/лестница)
|
||||
| LEVELDOOR_ENV_IDS # + дверь уровня (створка/лестница)
|
||||
| PALACE_ENV_IDS) # + куски, что рисует только палас
|
||||
wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL)
|
||||
|
||||
env_bg = [i for i in env_ids if i not in FORE_ENV_IDS]
|
||||
@@ -410,7 +486,7 @@ def main():
|
||||
total_bytes = 0
|
||||
for pg in range(max_pg + 1):
|
||||
if pg in pages:
|
||||
nm = f"pop_env{pg}.atl"
|
||||
nm = f"{prefix}env{pg}.atl"
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, nm), pages[pg])
|
||||
total_bytes += sz
|
||||
env_page_names.append(nm)
|
||||
@@ -421,18 +497,18 @@ def main():
|
||||
|
||||
# WALL -> idx=id
|
||||
wall_entries = {id: load_indexed(R.CHTAB_WALL, id) for id in wall_ids}
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_wall.atl"), wall_entries)
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, prefix + "wall.atl"), wall_entries)
|
||||
total_bytes += sz
|
||||
print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
print(f" {prefix}wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
# FORE -> idx=id
|
||||
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)
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, prefix + "fore.atl"), fore_entries)
|
||||
total_bytes += sz
|
||||
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
print(f" {prefix}fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
# POTION (chtab_1) -> idx=id: склянки + кадры пузырька + маска
|
||||
# chtab_1 целиком (res151..res173 = id 1..23): пламя факелов, меч-предмет,
|
||||
@@ -452,13 +528,15 @@ def main():
|
||||
pot_entries[id] = load_potion(id, opaque=True)
|
||||
else:
|
||||
pot_entries[id] = load_potion(id)
|
||||
# chtab_1 (пламя/меч/зелья) от тайлсета НЕ зависит — пакуем один раз.
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries)
|
||||
total_bytes += sz
|
||||
print(f" pop_pot.atl (chtab_1: факелы+зелья): {len(pot_entries)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
build_tileset_palette(os.path.join(OUT_DIR, prefix + "tile.pal"))
|
||||
print(f" {prefix}tile.pal: 32 записи (env 0x50..0x5F + wall 0x60..0x6F)")
|
||||
build_palette(os.path.join(OUT_DIR, "pop_bg.pal"))
|
||||
write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"),
|
||||
env_page_names, "pop_wall.atl", "pop_fore.atl")
|
||||
write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"), env_page_names)
|
||||
|
||||
n_pages = sum(1 for n in env_page_names if n) + 2
|
||||
print(f"ИТОГО: {len(env_bg)} env-фон + {len(wall_ids)} wall + "
|
||||
|
||||
Reference in New Issue
Block a user