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>
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pop_extract_kid_data.py — извлекает данные анимации Kid из исходников
|
||||
SDLPoP в C-заголовок roomtest/kid_data.h (Фаза K1).
|
||||
|
||||
Извлекает:
|
||||
frame_table_kid[] (seg006.c) -> kframe kid_frames[] {image,dx,dy,flags}
|
||||
original_seqtbl[] (seqtbl.c) -> byte kid_seqtbl[] (2310 Б, база 0x196E)
|
||||
original_seqtbl_offsets[] (seqtbl.c) -> word kid_seq_off[] (seq_id -> адрес)
|
||||
|
||||
seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_seqtbl[addr-0x196E].
|
||||
Автогенерация — чтобы не переписывать ~180 кадров + 2КБ байткода руками.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src"
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "roomtest", "kid_data.h")
|
||||
SEQTBL_BASE = 0x196E
|
||||
|
||||
|
||||
def read(name):
|
||||
return open(os.path.join(SRC, name)).read()
|
||||
|
||||
|
||||
def parse_frame_table(txt):
|
||||
"""frame_table_kid[] -> список (image, dx, dy, flags). Записи вида
|
||||
{ IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как OR-выражения."""
|
||||
m = re.search(r'const frame_type frame_table_kid\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
|
||||
body = m.group(1)
|
||||
frames = []
|
||||
for row in re.findall(r'\{([^}]*)\}', body):
|
||||
parts = [p.strip() for p in row.split(',')]
|
||||
image = int(parts[0], 0)
|
||||
sword = eval(parts[1], {}, {}) # напр. "0x00| 9"
|
||||
dx = int(parts[2], 0)
|
||||
dy = int(parts[3], 0)
|
||||
flags = eval(parts[4], {}, {})
|
||||
frames.append((image & 0xFF, dx & 0xFF, dy & 0xFF, flags & 0xFF, sword & 0xFF))
|
||||
return frames
|
||||
|
||||
|
||||
def parse_byte_array(txt, name):
|
||||
m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
|
||||
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
|
||||
|
||||
|
||||
def parse_word_array(txt, name):
|
||||
m = re.search(r'const word %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
|
||||
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
|
||||
|
||||
|
||||
def emit_c(vals, per_line=12, fmt="0x%02X"):
|
||||
out = []
|
||||
for i in range(0, len(vals), per_line):
|
||||
out.append(" " + ",".join(fmt % v for v in vals[i:i + per_line]) + ",")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
seg006 = read("seg006.c")
|
||||
seqc = read("seqtbl.c")
|
||||
|
||||
frames = parse_frame_table(seg006)
|
||||
seqtbl = parse_byte_array(seqc, "original_seqtbl")
|
||||
offs = parse_word_array(seqc, "original_seqtbl_offsets")
|
||||
|
||||
with open(OUT, "w") as f:
|
||||
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
|
||||
" * offsets), извлечено toolchain/pop_extract_kid_data.py из\n"
|
||||
" * SDLPoP. НЕ править вручную. seqtbl адресуется базой\n"
|
||||
" * SEQTBL_BASE: kid_seqtbl[addr - SEQTBL_BASE]. */\n")
|
||||
f.write("#ifndef KID_DATA_H\n#define KID_DATA_H\n#include <stdint.h>\n\n")
|
||||
f.write("#define SEQTBL_BASE 0x%04Xu\n\n" % SEQTBL_BASE)
|
||||
|
||||
# frame table
|
||||
f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */\n")
|
||||
f.write("typedef struct { uint8_t image; int8_t dx, dy; "
|
||||
"uint8_t flags, sword; } kframe;\n")
|
||||
f.write("#define KID_NFRAMES %d\n" % len(frames))
|
||||
f.write("static const kframe kid_frames[KID_NFRAMES] = {\n")
|
||||
for (img, dx, dy, fl, sw) in frames:
|
||||
dxs = dx - 256 if dx > 127 else dx
|
||||
dys = dy - 256 if dy > 127 else dy
|
||||
f.write(" {%3d,%4d,%4d,0x%02X,0x%02X},\n" % (img, dxs, dys, fl, sw))
|
||||
f.write("};\n\n")
|
||||
|
||||
# seqtbl bytes
|
||||
f.write("#define KID_SEQTBL_LEN %d\n" % len(seqtbl))
|
||||
f.write("static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {\n")
|
||||
f.write(emit_c(seqtbl) + "\n};\n\n")
|
||||
|
||||
# seq offsets
|
||||
f.write("#define KID_NSEQ %d\n" % len(offs))
|
||||
f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n")
|
||||
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
|
||||
f.write("#endif\n")
|
||||
|
||||
print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, "
|
||||
f"{len(offs)} seq-оффсетов -> {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pop_pack_bg.py — упаковщик спрайтов СТАТИЧЕСКОГО ФОНА PoP (Шаг 2 порта).
|
||||
|
||||
Что делает:
|
||||
1. Определяет РЕАЛЬНО используемый набор спрайтов фона (прогоняет
|
||||
render_room.py по всем комнатам всех уровней, собирает (chtab,id)).
|
||||
2. Грузит исходные PNG (VDUNGEON→VPALACE каскад, как в игре), переводит
|
||||
16-цветный индексированный пиксель в 8bpp getimage-блоб:
|
||||
индекс 0 -> 0xFF (прозрачность getimage)
|
||||
индекс i -> 0x50+i (env, chtab_6) / 0x60+i (wall, chtab_7)
|
||||
3. Пакует в атласы .atl с ПРЯМОЙ адресацией (idx = id), каталог любого
|
||||
размера (блобы кладутся ПОСЛЕ каталога — загрузчик atlas_image читает
|
||||
offset явно, лимит 19 из libbgi/mkatlas.py не действует). libbgi НЕ
|
||||
трогаем. Дырки в каталоге (неиспользуемые id) = фейковые записи,
|
||||
ссылающиеся на общий пустой 0×0-блоб (осознанная трата места в EMM
|
||||
ради нулевых remap-таблиц в W2 — требование пользователя).
|
||||
4. Собирает палитру Sprinter (.pal, записи B,G,R,0) из res200.pal (env
|
||||
-> слоты 0x50..0x5F) и res360.pal (wall -> 0x60..0x6F). VGA 6-бит
|
||||
каналы масштабируются в 8-бит.
|
||||
5. Пишет C-заголовок с раскладкой (имена файлов, SHIFT, число страниц,
|
||||
набор fore-id).
|
||||
|
||||
Раскладка (прямая адресация, SHIFT=5):
|
||||
ENV фон : 5 страниц, страница = id>>5, idx = id&31
|
||||
WALL : 1 страница, idx = id
|
||||
FORE : 1 страница (спрайты «перед персонажем»: фронты столбов/
|
||||
ворот/дебриса), idx = id — отдельный атлас (жизненный цикл
|
||||
другой: рисуются каждый кадр как sprite_t, не запекаются).
|
||||
W2-цена: 7 хэндлов atlas_t (~21 Б), ноль таблиц id->позиция.
|
||||
|
||||
Выход: в applications/PoP/poc/res/bg/ — *.atl + pop_bg.pal + pop_bg_atlas.h
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import render_room as R # noqa: E402 (экстрактор + каскад загрузки)
|
||||
|
||||
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}
|
||||
|
||||
ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31
|
||||
ENV_MASK = (1 << ENV_SHIFT) - 1
|
||||
|
||||
# ---- .atl формат (== libbgi, но каталог не ограничен 19) --------------
|
||||
ATL_MAGIC = b"SPA1"
|
||||
ATL_DIR_OFF = 0x68 # _ATL_DIR_OFF
|
||||
ATL_MIN_SIZE = 0x100 # atlas_load требует n >= _ATL_DATA_OFF
|
||||
PAGE_SIZE = 16384
|
||||
|
||||
|
||||
def die(msg):
|
||||
sys.exit(f"pop_pack_bg: {msg}")
|
||||
|
||||
|
||||
# ---- 1. Набор используемых спрайтов ----------------------------------
|
||||
def collect_used():
|
||||
"""Прогон render_room по всем комнатам -> множество (chtab, id)."""
|
||||
used = set()
|
||||
orig = R.load_sprite
|
||||
|
||||
def spy(id, chtab=R.CHTAB_ENV, reason=""):
|
||||
img = orig(id, chtab, reason)
|
||||
if img is not None and id > 0:
|
||||
used.add((chtab, id))
|
||||
return img
|
||||
|
||||
R.load_sprite = spy
|
||||
for lvl in range(1, 15):
|
||||
fn = os.path.join(R.LEVEL_DIR, f"res{2000 + lvl}.bin")
|
||||
if not os.path.exists(fn):
|
||||
continue
|
||||
lv = R.Level(open(fn, "rb").read())
|
||||
for room in range(1, 25):
|
||||
rm = R.Room(lv, room)
|
||||
L = lv.links[room][0]
|
||||
if L:
|
||||
rm.left_room = R.Room(lv, L)
|
||||
canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0))
|
||||
for dr in (2, 1, 0):
|
||||
for c in range(10):
|
||||
R.draw_tile(canvas, rm, dr, c)
|
||||
R.load_sprite = orig
|
||||
return used
|
||||
|
||||
|
||||
# ---- 2. Загрузка спрайта в 8bpp-блоб ---------------------------------
|
||||
def load_indexed(chtab, id):
|
||||
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
|
||||
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60)."""
|
||||
base_res = 360 if chtab == R.CHTAB_WALL else 200
|
||||
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):
|
||||
p = os.path.join(d, fname)
|
||||
if os.path.exists(p):
|
||||
path = p
|
||||
break
|
||||
if path is None:
|
||||
die(f"нет файла {fname}")
|
||||
im = Image.open(path)
|
||||
if im.mode != "P":
|
||||
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
|
||||
src = im.tobytes()
|
||||
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
|
||||
return im.width, im.height, out
|
||||
|
||||
|
||||
def blob(w, h, pix):
|
||||
"""getimage-блоб: u16 w, u16 h (LE) + пиксели."""
|
||||
return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix
|
||||
|
||||
|
||||
# ---- 3. Пакер .atl (каталог любого размера) --------------------------
|
||||
def pack_atlas(path, entries):
|
||||
"""entries: dict idx -> (w, h, pix). Пустые idx (0..maxidx) заполняются
|
||||
фейковой ссылкой на общий 0×0-блоб. Каталог count записей с 0x68,
|
||||
блобы — сразу после каталога."""
|
||||
if not entries:
|
||||
die(f"{path}: пустой атлас")
|
||||
count = max(entries) + 1
|
||||
if count > 255:
|
||||
die(f"{path}: count {count} > 255")
|
||||
data_off = ATL_DIR_OFF + count * 8
|
||||
|
||||
blobs = bytearray()
|
||||
empty_off = data_off + len(blobs) # общий 0×0-блоб для дырок
|
||||
blobs += blob(0, 0, b"")
|
||||
dir_bytes = bytearray(count * 8)
|
||||
for idx in range(count):
|
||||
e = entries.get(idx)
|
||||
if e is None:
|
||||
off = empty_off
|
||||
fw = fh = 0
|
||||
else:
|
||||
w, h, pix = e
|
||||
off = data_off + len(blobs)
|
||||
blobs += blob(w, h, pix)
|
||||
fw = min(w, 255)
|
||||
fh = min(h, 255)
|
||||
d = idx * 8
|
||||
dir_bytes[d:d + 2] = off.to_bytes(2, "little")
|
||||
dir_bytes[d + 2] = fw
|
||||
dir_bytes[d + 3] = fh
|
||||
dir_bytes[d + 4] = 1 # nx
|
||||
dir_bytes[d + 5] = 1 # ny
|
||||
# d+6,d+7 резерв
|
||||
|
||||
out = bytearray(data_off)
|
||||
out[0:4] = ATL_MAGIC
|
||||
out[4] = count
|
||||
out[ATL_DIR_OFF:ATL_DIR_OFF + len(dir_bytes)] = dir_bytes
|
||||
out += blobs
|
||||
if len(out) < ATL_MIN_SIZE:
|
||||
out += bytes(ATL_MIN_SIZE - len(out)) # atlas_load: n >= 0x100
|
||||
if len(out) > PAGE_SIZE:
|
||||
die(f"{path}: {len(out)} Б > {PAGE_SIZE} (одна EMM-страница)")
|
||||
with open(path, "wb") as f:
|
||||
f.write(out)
|
||||
return count, len(out)
|
||||
|
||||
|
||||
# ---- 4. Палитра ------------------------------------------------------
|
||||
def scale6to8(v):
|
||||
return (v * 255) // 63
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
env = read_pal("res200.pal", R.VDUNGEON_DIR)
|
||||
wall = read_pal("res360.pal", R.VDUNGEON_DIR)
|
||||
pal = bytearray(256 * 4)
|
||||
for i, (r, g, b) in enumerate(env):
|
||||
o = (0x50 + i) * 4
|
||||
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
for i, (r, g, b) in enumerate(wall):
|
||||
o = (0x60 + i) * 4
|
||||
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(pal)
|
||||
|
||||
|
||||
# ---- 5. C-заголовок --------------------------------------------------
|
||||
def write_header(out_path, env_pages, wall_name, fore_name):
|
||||
lines = [
|
||||
"/* pop_bg_atlas.h — раскладка атласов статического фона PoP.",
|
||||
" * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.",
|
||||
" *",
|
||||
" * Прямая адресация (ноль remap-таблиц в W2):",
|
||||
" * 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",
|
||||
" */",
|
||||
"#ifndef POP_BG_ATLAS_H",
|
||||
"#define POP_BG_ATLAS_H",
|
||||
"",
|
||||
"#define POP_ENV_SHIFT %d" % ENV_SHIFT,
|
||||
"#define POP_ENV_MASK %d" % ENV_MASK,
|
||||
"#define POP_ENV_PAGES %d" % len(env_pages),
|
||||
"",
|
||||
"/* Палитра: 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] = {",
|
||||
]
|
||||
for pg in range(len(env_pages)):
|
||||
nm = env_pages[pg]
|
||||
lines.append(' %s,' % ('"%s"' % nm if nm else "0 /* пусто */"))
|
||||
lines += [
|
||||
"};",
|
||||
'#define POP_WALL_ATL "%s"' % wall_name,
|
||||
'#define POP_FORE_ATL "%s"' % fore_name,
|
||||
'#define POP_BG_PAL "pop_bg.pal"',
|
||||
"",
|
||||
"#endif",
|
||||
"",
|
||||
]
|
||||
with open(out_path, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
used = collect_used()
|
||||
env_ids = sorted(id for ch, id in used if ch != R.CHTAB_WALL)
|
||||
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]
|
||||
env_fore = [i for i in env_ids if i in FORE_ENV_IDS]
|
||||
|
||||
# ENV фон -> страницы по id>>SHIFT
|
||||
pages = {}
|
||||
for id in env_bg:
|
||||
w, h, pix = load_indexed(R.CHTAB_ENV, id)
|
||||
pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
|
||||
max_pg = max(pages) if pages else -1
|
||||
env_page_names = []
|
||||
total_bytes = 0
|
||||
for pg in range(max_pg + 1):
|
||||
if pg in pages:
|
||||
nm = f"pop_env{pg}.atl"
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, nm), pages[pg])
|
||||
total_bytes += sz
|
||||
env_page_names.append(nm)
|
||||
print(f" {nm}: {len(pages[pg])} спрайтов, каталог {cnt}, {sz} Б")
|
||||
else:
|
||||
env_page_names.append(None)
|
||||
print(f" (env стр {pg}: пусто)")
|
||||
|
||||
# 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)
|
||||
total_bytes += sz
|
||||
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}
|
||||
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} Б")
|
||||
|
||||
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")
|
||||
|
||||
n_pages = sum(1 for n in env_page_names if n) + 2
|
||||
print(f"ИТОГО: {len(env_bg)} env-фон + {len(wall_ids)} wall + "
|
||||
f"{len(env_fore)} fore = {len(env_bg)+len(wall_ids)+len(env_fore)} спрайтов")
|
||||
print(f" {n_pages} EMM-страниц, {total_bytes} Б на диске "
|
||||
f"(+1КБ палитра), W2: {n_pages}×atlas_t = {n_pages*3} Б хэндлов")
|
||||
print(f" -> {OUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pop_pack_kid.py — упаковщик спрайтов персонажа Kid (Фаза K0 порта).
|
||||
|
||||
Аналог pop_pack_bg.py, но для chtab_2_kid (база ресурса 400): image id N
|
||||
= res(400+N).png (data/KID), палитра res400.pal (16 цветов) -> слоты
|
||||
Sprinter 0x70-0x7F (env 0x50 / wall 0x60 заняты фоном). Пиксель i:
|
||||
0->0xFF (прозрач), i->0x70+i.
|
||||
|
||||
Прямая адресация (ноль remap в W2): kid[img>>5], idx img&31. Дырки —
|
||||
фейковые записи (как в фоне). Переиспользует pack_atlas/blob/scale6to8
|
||||
из pop_pack_bg.
|
||||
|
||||
Выход: poc/res/kid/kid0..N.atl + kid.pal + kid_atlas.h
|
||||
"""
|
||||
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 # pack_atlas, blob, scale6to8, ENV_SHIFT
|
||||
|
||||
KID_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/KID"
|
||||
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
|
||||
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
|
||||
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
|
||||
MASK = (1 << SHIFT) - 1
|
||||
|
||||
|
||||
def transpose_cols(w, h, rowmajor):
|
||||
"""row-major (pix[row*w+col]) -> column-major (out[col*h+row]).
|
||||
Спрайты Kid хранятся КОЛОНКАМИ: gfx_blit_cols рисует вертикальным accel-
|
||||
проходом (даёт бесплатный горизонтальный флип реверсом порядка колонок;
|
||||
см. memory/accel_vertical_copy). Заголовок getimage (w,h) не меняется."""
|
||||
out = bytearray(w * h)
|
||||
for col in range(w):
|
||||
base = col * h
|
||||
for row in range(h):
|
||||
out[base + row] = rowmajor[row * w + col]
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def load_kid(aidx):
|
||||
"""Атлас-индекс aidx (== frame.image) -> res(401+aidx).png -> (w, h, 8bpp
|
||||
COLUMN-MAJOR). 0->0xFF, i->0x70+i. Пиксели транспонированы в колонки.
|
||||
|
||||
МАППИНГ (seg008.c:1589): персонаж рисуется add_midtable(chtab, obj_id+1),
|
||||
внутри get_image(chtab, id-1)=get_image(chtab, obj_id) -> chtab-индекс =
|
||||
frame.image; первая картинка chtab_2_kid = res401 (res400 = палитра).
|
||||
Значит frame.image=N -> res(401+N). Раньше грузили res(400+N) — off-by-one:
|
||||
стойка frame_15 (image 14) рисовалась res414 (беговой наклон) вместо res415
|
||||
(прямая стойка)."""
|
||||
path = os.path.join(KID_DIR, f"res{401 + aidx}.png")
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
im = Image.open(path)
|
||||
if im.mode != "P":
|
||||
B.die(f"res{401+aidx}: 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)
|
||||
|
||||
|
||||
def _read16(path):
|
||||
b = open(path, "rb").read()
|
||||
return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]
|
||||
|
||||
|
||||
def build_palette(out_path):
|
||||
"""ЕДИНАЯ игровая палитра (gfx_pal_fload пишет с индекса 0, перезатирая
|
||||
всё — bg и Kid обязаны жить в одном файле): env@0x50, wall@0x60, kid@0x70.
|
||||
"""
|
||||
pal = bytearray(256 * 4)
|
||||
groups = [
|
||||
(0x50, os.path.join(B.R.VDUNGEON_DIR, "res200.pal")), # env
|
||||
(0x60, os.path.join(B.R.VDUNGEON_DIR, "res360.pal")), # wall
|
||||
(PAL_BASE, os.path.join(KID_DIR, "res400.pal")), # kid 0x70
|
||||
]
|
||||
for base, path in groups:
|
||||
for i, (r, g, bl) in enumerate(_read16(path)):
|
||||
o = (base + i) * 4
|
||||
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(pal)
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
# атлас-индексы aidx (== frame.image), для которых есть res(401+aidx).png
|
||||
ids = []
|
||||
for aidx in range(0, 255):
|
||||
if os.path.exists(os.path.join(KID_DIR, f"res{401 + aidx}.png")):
|
||||
ids.append(aidx)
|
||||
if not ids:
|
||||
B.die("нет спрайтов KID")
|
||||
|
||||
pages = {}
|
||||
for id in ids:
|
||||
w, h, pix = load_kid(id)
|
||||
pages.setdefault(id >> SHIFT, {})[id & MASK] = (w, h, pix)
|
||||
|
||||
max_pg = max(pages)
|
||||
names = []
|
||||
total = 0
|
||||
for pg in range(max_pg + 1):
|
||||
if pg in pages:
|
||||
nm = f"kid{pg}.atl"
|
||||
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages[pg])
|
||||
total += sz
|
||||
names.append(nm)
|
||||
print(f" {nm}: {len(pages[pg])} спрайтов, каталог {cnt}, {sz} Б")
|
||||
else:
|
||||
names.append(None)
|
||||
print(f" (kid стр {pg}: пусто)")
|
||||
|
||||
build_palette(os.path.join(OUT_DIR, "kid.pal"))
|
||||
|
||||
# C-заголовок
|
||||
hdr = os.path.join(OUT_DIR, "kid_atlas.h")
|
||||
with open(hdr, "w") as f:
|
||||
f.write("/* kid_atlas.h — раскладка атласов Kid. Сгенерировано "
|
||||
"pop_pack_kid.py. */\n#ifndef KID_ATLAS_H\n#define KID_ATLAS_H\n")
|
||||
f.write(f"#define KID_SHIFT {SHIFT}\n#define KID_MASK {MASK}\n")
|
||||
f.write(f"#define KID_PAGES {len(names)}\n#define KID_PAL 0x{PAL_BASE:02X}\n")
|
||||
f.write("static const char *const kid_atl[KID_PAGES] = {\n")
|
||||
for nm in names:
|
||||
f.write(" %s,\n" % ('"%s"' % nm if nm else "0"))
|
||||
f.write('};\n#define KID_PAL_FILE "kid.pal"\n#endif\n')
|
||||
|
||||
print(f"ИТОГО: {len(ids)} спрайтов Kid, {len(names)} EMM-страниц, "
|
||||
f"{total} Б диск (+1КБ палитра) -> {OUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,842 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parse SDLPoP level file (res2001.bin) and render the first room by
|
||||
faithfully replaying the tile-drawing logic of seg008.c (draw_room/draw_tile).
|
||||
|
||||
Pixel model (from seg008.c):
|
||||
- screen is 320px wide, 10 columns.
|
||||
- col_xh[col] = {0,4,8,12,16,20,24,28,32,36}; x_px = xh*8 -> 0,32,64,...,288
|
||||
(tile column is 32px; the 4px offset handles overlap of floor/wall edges)
|
||||
- rows drawn 2,1,0 (bottom..top). For each row:
|
||||
draw_bottom_y = 63*drawn_row + 65
|
||||
draw_main_y = draw_bottom_y - 3
|
||||
row2 -> bottom_y=191, row1 -> 128, row0 -> 65
|
||||
- a tile sprite of height h drawn at ybottom is placed with its BOTTOM at ybottom:
|
||||
y = ybottom - h + 1
|
||||
|
||||
We replay draw_tile() which composites (in order):
|
||||
floorright, anim_topright, right, anim_right, bottom, loose, base, anim, fore
|
||||
Dependencies on NEIGHBOURS:
|
||||
- tile_left = tile in the column to the left (same room, same row)
|
||||
- row_below_left_[col] = tile in (room_below, col-1, row_below) -> for topright/floorright
|
||||
- For a single room with no links we treat out-of-range neighbours as walls
|
||||
(level edges), matching SDLPoP's custom->drawn_tile_*_level_edge defaults
|
||||
(left = wall, top = floor).
|
||||
|
||||
We ignore animated variants (spike frames, chomper, torch flames, gate
|
||||
animation) for this first static render; they use curr_modifier/backtable.
|
||||
"""
|
||||
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
LEVEL_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/LEVELS"
|
||||
VPALACE_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VPALACE"
|
||||
VDUNGEON_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VDUNGEON"
|
||||
|
||||
# For dungeon levels (tbl_level_type == 0) the environment sprites come from
|
||||
# VDUNGEON where present, otherwise fall back to the shared VPALACE set
|
||||
# (PRINCE.DAT common images). For palace levels use VPALACE only.
|
||||
IS_DUNGEON = True # room 1 (res2001) is the dungeon
|
||||
|
||||
# tile_table[code] = (base_id, floor_left, base_y, right_id, floor_right,
|
||||
# right_y, stripe_id, topright_id, bottom_id, fore_id,
|
||||
# fore_x, fore_y)
|
||||
TILE_TABLE = [
|
||||
( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), # 0x00 empty
|
||||
( 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x01 floor
|
||||
(127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0), # 0x02 spike
|
||||
( 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0), # 0x03 pillar
|
||||
( 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0), # 0x04 gate
|
||||
( 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0), # 0x05 stuck floor
|
||||
( 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0), # 0x06 close button
|
||||
( 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0), # 0x07 door top with floor
|
||||
( 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0), # 0x08 big pillar bottom
|
||||
( 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3), # 0x09 big pillar top
|
||||
( 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, 2, -3), # 0x0A potion
|
||||
( 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0), # 0x0B loose floor
|
||||
( 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0), # 0x0C door top
|
||||
( 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0), # 0x0D mirror
|
||||
( 97, 1, 0, 98, 1, 2, 145, 0, 43, 100, 0, 0), # 0x0E debris
|
||||
(147, 1, 0, 42, 1, 1, 145, 0, 149, 0, 0, 0), # 0x0F open button
|
||||
( 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0), # 0x10 leveldoor left
|
||||
( 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0), # 0x11 leveldoor right
|
||||
( 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x12 chomper
|
||||
( 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0), # 0x13 torch
|
||||
( 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0), # 0x14 wall
|
||||
( 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0), # 0x15 skeleton
|
||||
( 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x16 sword
|
||||
( 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0), # 0x17 balcony left
|
||||
( 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0), # 0x18 balcony right
|
||||
( 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0), # 0x19 lattice pillar
|
||||
( 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0, -53), # 0x1A lattice down
|
||||
( 3, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1B lattice small
|
||||
( 4, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1C lattice left
|
||||
( 5, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1D lattice right
|
||||
( 97, 1, 0, 98, 1, 2, 0, 0, 43, 100, 0, 0), # 0x1E debris with torch
|
||||
]
|
||||
|
||||
TILE_NAMES = {
|
||||
0x00: "empty", 0x01: "floor", 0x02: "spike", 0x03: "pillar",
|
||||
0x04: "gate", 0x05: "stuck_floor", 0x06: "close_button", 0x07: "door_top_floor",
|
||||
0x08: "big_pillar_bottom", 0x09: "big_pillar_top", 0x0A: "potion",
|
||||
0x0B: "loose_floor", 0x0C: "door_top", 0x0D: "mirror", 0x0E: "debris",
|
||||
0x0F: "open_button", 0x10: "leveldoor_left", 0x11: "leveldoor_right",
|
||||
0x12: "chomper", 0x13: "torch", 0x14: "wall", 0x15: "skeleton",
|
||||
0x16: "sword", 0x17: "balcony_left", 0x18: "balcony_right",
|
||||
0x19: "lattice_pillar", 0x1A: "lattice_down", 0x1B: "lattice_small",
|
||||
0x1C: "lattice_left", 0x1D: "lattice_right", 0x1E: "debris_torch",
|
||||
}
|
||||
|
||||
COL_XH = [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]
|
||||
|
||||
# chtab -> directory/subfolder + id offset
|
||||
# id_chtab_6_environment -> VPALACE, res(200+id).png
|
||||
# id_chtab_7_environmentwall -> VPALACE (wall images also here, res(200+id))
|
||||
# id_chtab_1_flameswordpotion -> flames etc (skipped in static render)
|
||||
CHTAB_ENV = 6
|
||||
CHTAB_WALL = 7
|
||||
CHTAB_FSP = 1 # flames/sword/potion (animated, skipped)
|
||||
|
||||
|
||||
# ---- POP1 level file structure (see POP-DAT-FormatSpecifications.txt, sec 3.4) ----
|
||||
# Block layout (for the standard 2305-byte level):
|
||||
# 0 .. 720 pop1 foretable (24 rooms x 30 bytes)
|
||||
# 720 .. 1440 pop1 backtable (24 rooms x 30 bytes)
|
||||
# 1440 .. 1696 door I (256 bytes, one per event line)
|
||||
# 1696 .. 1952 door II (256 bytes)
|
||||
# 1952 .. 2048 links (24 rooms x 4 bytes: L,R,Up,Dn)
|
||||
# 2048 .. 2112 unknown I
|
||||
# 2112 .. 2115 start position (room, location, direction)
|
||||
# 2115 .. 2116 unknown II
|
||||
# 2116 .. 2119 unknown III
|
||||
# 2119 .. 2143 guard location (24)
|
||||
# 2143 .. 2167 guard direction (24)
|
||||
# 2167 .. 2191 unknown IV (a)
|
||||
# 2191 .. 2215 unknown IV (b)
|
||||
# 2215 .. 2239 guard skill (24)
|
||||
# 2239 .. 2263 unknown IV (c)
|
||||
# 2263 .. 2287 guard colour (24)
|
||||
# 2287 .. 2303 unknown IV (d)
|
||||
# 2303 .. 2305 0F 09
|
||||
|
||||
# foretable byte format: rrmccccc (2 random bits, 1 modifier bit, 5 code bits)
|
||||
FG_RAND_MASK = 0xC0
|
||||
FG_MODBIT_MASK = 0x20
|
||||
FG_CODE_MASK = 0x1F
|
||||
|
||||
# backtable: a tile modifier that depends on the group of the foretable code.
|
||||
# group ids map foretable code -> "group" used to interpret backtable value.
|
||||
# (see Table 7/8). We only need the group for door/event interpretation.
|
||||
GROUP_OF_CODE = {
|
||||
0x00: "free", 0x01: "free", 0x02: "spike", 0x03: "none",
|
||||
0x04: "gate", 0x05: "none", 0x06: "event", 0x07: "tapest",
|
||||
0x08: "none", 0x09: "none", 0x0A: "potion", 0x0B: "none",
|
||||
0x0C: "ttop", 0x0D: "none", 0x0E: "none", 0x0F: "event",
|
||||
0x10: "none", 0x11: "none", 0x12: "chomp", 0x13: "none",
|
||||
0x14: "wall", 0x15: "none", 0x16: "none", 0x17: "none",
|
||||
0x18: "none", 0x19: "none", 0x1A: "none", 0x1B: "none",
|
||||
0x1C: "none", 0x1D: "none", 0x1E: "none", 0x1F: "none",
|
||||
}
|
||||
|
||||
# backtable modifier meanings per group (subset, from Table 8):
|
||||
GATE_MOD = {0: "closed", 1: "open", 2: "with_lattice", 3: "alt_design"}
|
||||
TTOP_MOD = {0: "normal", 1: "black", 2: "empty", 3: "?3", 4: "?4", 5: "?5", 6: "?6", 7: "?7"}
|
||||
SPIKE_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out"}
|
||||
CHOMP_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out", 4: "out?"}
|
||||
POTION_MOD = {0: "blue", 1: "no_blue", 2: "window", 3: "spot?"}
|
||||
TAPEST_MOD = {0: "normal", 1: "alt_design", 2: "black", 3: "empty"}
|
||||
EVENT_MOD_DESC = { # group "event": the backtable byte IS the event id
|
||||
0: "closer", 1: "opener",
|
||||
}
|
||||
|
||||
|
||||
class Level:
|
||||
"""Full parsed POP1 level file."""
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
self.rooms_fg = {} # room -> list[30] of (fg_byte, code, modbit)
|
||||
self.rooms_bg = {} # room -> list[30] of backtable byte
|
||||
for room in range(1, 25):
|
||||
fg = []; bg = []
|
||||
for off in range(30):
|
||||
i = (room - 1) * 30 + off
|
||||
fb = data[i]; bb = data[720 + i]
|
||||
fg.append((fb, fb & FG_CODE_MASK, (fb & FG_MODBIT_MASK) >> 5))
|
||||
bg.append(bb)
|
||||
self.rooms_fg[room] = fg
|
||||
self.rooms_bg[room] = bg
|
||||
# door I / II (event lines 0..255)
|
||||
self.door_i = data[1440:1696]
|
||||
self.door_ii = data[1696:1952]
|
||||
# links: 24 rooms x (L,R,Up,Dn)
|
||||
self.links = {}
|
||||
for room in range(1, 25):
|
||||
b = data[1952 + (room - 1) * 4: 1952 + (room - 1) * 4 + 4]
|
||||
self.links[room] = tuple(b) # (left, right, up, down)
|
||||
# start position
|
||||
self.start = tuple(data[2112:2115])
|
||||
# guards
|
||||
self.guard_loc = list(data[2119:2143])
|
||||
self.guard_dir = list(data[2143:2167])
|
||||
self.guard_skill = list(data[2215:2239])
|
||||
self.guard_colour = list(data[2263:2287])
|
||||
|
||||
def door_for_event(self, event_id):
|
||||
"""Decode door I/II for an event line -> (room, location, trigger_next)."""
|
||||
bi = self.door_i[event_id]
|
||||
bii = self.door_ii[event_id]
|
||||
# Byte I: t1 s4 s5 l1 l2 l3 l4 l5
|
||||
t1 = (bi >> 7) & 1
|
||||
s4 = (bi >> 6) & 1
|
||||
s5 = (bi >> 5) & 1
|
||||
l1 = (bi >> 4) & 1
|
||||
l2 = (bi >> 3) & 1
|
||||
l3 = (bi >> 2) & 1
|
||||
l4 = (bi >> 1) & 1
|
||||
l5 = bi & 1
|
||||
# Byte II: s1 s2 s3 00000
|
||||
s1 = (bii >> 7) & 1
|
||||
s2 = (bii >> 6) & 1
|
||||
s3 = (bii >> 5) & 1
|
||||
screen = (s1 << 4) | (s2 << 3) | (s3 << 2) | (s4 << 1) | s5
|
||||
loc = (l1 << 4) | (l2 << 3) | (l3 << 2) | (l4 << 1) | l5
|
||||
return screen, loc, t1
|
||||
|
||||
def backtable_desc(self, code, bb):
|
||||
grp = GROUP_OF_CODE.get(code, "none")
|
||||
tbl = {"gate": GATE_MOD, "ttop": TTOP_MOD, "spike": SPIKE_MOD,
|
||||
"chomp": CHOMP_MOD, "potion": POTION_MOD, "tapest": TAPEST_MOD}
|
||||
if code == 0x06 or code == 0x0F: # event group
|
||||
act = EVENT_MOD_DESC.get(bb & 1, "?")
|
||||
return f"event(id={bb}, {act})"
|
||||
if grp in tbl:
|
||||
return tbl[grp].get(bb, f"?0x{bb:02X}")
|
||||
return f"0x{bb:02X}"
|
||||
|
||||
|
||||
class Room:
|
||||
def __init__(self, level, room_num):
|
||||
self.level = level
|
||||
self.room_num = room_num
|
||||
self.fg = level.rooms_fg[room_num]
|
||||
self.bg = level.rooms_bg[room_num]
|
||||
self.left_room = None # соседняя комната слева (link L); load_leftroom
|
||||
|
||||
def tile(self, row, col):
|
||||
"""Return (code, fg_byte, modbit, backtable_byte) for an in-room tile.
|
||||
Out-of-range neighbours follow seg008 defaults:
|
||||
col<0 -> берётся из ЛЕВОЙ комнаты (col 9), как load_leftroom();
|
||||
если левой комнаты нет — уровневая кромка = стена (20).
|
||||
row<0 -> top level edge = floor (code 1, mod 0)
|
||||
"""
|
||||
if col < 0:
|
||||
if self.left_room is not None and 0 <= row < 3:
|
||||
return self.left_room.tile(row, 9)
|
||||
return 20, 0, 0, 0
|
||||
if row < 0:
|
||||
return 1, 0, 0, 0
|
||||
if not (0 <= row < 3 and 0 <= col < 10):
|
||||
return 20, 0, 0, 0
|
||||
i = row * 10 + col
|
||||
fb, code, mbit = self.fg[i]
|
||||
return code, fb, mbit, self.bg[i]
|
||||
|
||||
|
||||
# Log of every sprite loaded during a render, for the per-tile report.
|
||||
SPRITE_LOG = []
|
||||
|
||||
|
||||
def load_sprite(id, chtab=CHTAB_ENV, reason=""):
|
||||
"""Load resource PNG для картинки id из набора chtab.
|
||||
|
||||
База ресурса зависит от chtab (seg000.c:1109/1122):
|
||||
chtab_6 environment -> res(200+id)
|
||||
chtab_7 environmentwall -> res(360+id)
|
||||
chtab_1 flame/sword/pot -> res(150+id) (анимация, статикой не грузим)
|
||||
|
||||
Dungeon (IS_DUNGEON) cascade: try VDUNGEON first (dungeon-specific
|
||||
overrides), then fall back to VPALACE (shared base set).
|
||||
"""
|
||||
if id <= 0:
|
||||
return None
|
||||
base = {CHTAB_ENV: 200, CHTAB_WALL: 360, CHTAB_FSP: 150}.get(chtab, 200)
|
||||
fname = f"res{base + id}.png"
|
||||
if IS_DUNGEON:
|
||||
vd = os.path.join(VDUNGEON_DIR, fname)
|
||||
if os.path.exists(vd):
|
||||
try:
|
||||
img = Image.open(vd).convert("RGBA")
|
||||
CURRENT_TILE_LOG.append((id, "VDUNGEON", reason))
|
||||
return img
|
||||
except Exception:
|
||||
pass
|
||||
vp = os.path.join(VPALACE_DIR, fname)
|
||||
try:
|
||||
img = Image.open(vp).convert("RGBA")
|
||||
CURRENT_TILE_LOG.append((id, "VPALACE", reason))
|
||||
return img
|
||||
except FileNotFoundError:
|
||||
CURRENT_TILE_LOG.append((id, "MISSING", reason))
|
||||
return None
|
||||
|
||||
|
||||
def blit(canvas, img, x, y):
|
||||
"""Composite img onto canvas at (x,y) using its alpha as mask (draw_main style)."""
|
||||
if img is None:
|
||||
return
|
||||
w, h = img.size
|
||||
if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height:
|
||||
return
|
||||
canvas.alpha_composite(img, (x, y))
|
||||
|
||||
|
||||
def blit_black(canvas, img, x, y):
|
||||
"""blitters_9_black (seg008.c:1054): рисует МАСКУ спрайта ЧЁРНЫМ —
|
||||
непрозрачная область формы заливается чёрным (тень-силуэт), а не
|
||||
пикселями картинки. Используется для floor-right в draw_tile_floorright
|
||||
(тень уступа тайла снизу-слева)."""
|
||||
if img is None:
|
||||
return
|
||||
w, h = img.size
|
||||
if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height:
|
||||
return
|
||||
alpha = img.split()[3]
|
||||
solid = Image.new("RGBA", img.size, (0, 0, 0, 255))
|
||||
canvas.alpha_composite(Image.composite(solid, Image.new("RGBA", img.size, (0, 0, 0, 0)), alpha), (x, y))
|
||||
|
||||
|
||||
# ---- Dungeon wall_pattern() (seg008.c:1928) ----
|
||||
# PRNG: seg009.c:321 random_seed = random_seed*214013 + 2531011; return (random_seed>>16) % (max+1)
|
||||
_random_seed = 0
|
||||
|
||||
|
||||
def prandom(maxval):
|
||||
global _random_seed
|
||||
_random_seed = (_random_seed * 214013 + 2531011) & 0xFFFFFFFF
|
||||
return (_random_seed >> 16) % (maxval + 1)
|
||||
|
||||
|
||||
# wall sprite ids (chtab_7 / RSET_WALL = 7) res(200+id).png
|
||||
RES_WALL_FACE_MAIN, RES_WALL_FACE_TOP, RES_WALL_CENTRE_BASE, RES_WALL_CENTRE_MAIN = 1, 2, 3, 4
|
||||
RES_WALL_RIGHT_BASE, RES_WALL_RIGHT_MAIN, RES_WALL_SINGLE_BASE, RES_WALL_SINGLE_MAIN = 5, 6, 7, 8
|
||||
RES_WALL_LEFT_BASE, RES_WALL_LEFT_MAIN, RES_WALL_DIVIDER1, RES_WALL_DIVIDER2 = 9, 10, 11, 12
|
||||
RES_WALL_RNDBLOCK = 13
|
||||
RES_WALL_MARK_TL, RES_WALL_MARK_BL, RES_WALL_MARK_TR, RES_WALL_MARK_BR = 14, 15, 16, 17
|
||||
|
||||
WALL_MODIFIER_SWS, WALL_MODIFIER_SWW, WALL_MODIFIER_WWS, WALL_MODIFIER_WWW = 0, 1, 2, 3
|
||||
|
||||
# Сплошная грань стены (chtab_7), индекс = wall_modifier (seg008.c:567/687):
|
||||
WALL_FRAM_BOTTOM = [7, 9, 5, 3] # нижняя часть (draw_tile_bottom)
|
||||
WALL_FRAM_MAIN = [8, 10, 6, 4] # основная часть (draw_tile_fore)
|
||||
|
||||
LPOS = [58, 41, 37, 20, 16] # draw_left_mark vertical offsets
|
||||
RPOS = [52, 42, 31, 21] # draw_right_mark vertical offsets
|
||||
|
||||
TBL_LINE = [0, 10, 20] # смещение строки в 30-тайловом массиве комнаты
|
||||
|
||||
|
||||
def wall_modifier(room, drawn_row, col):
|
||||
"""Модификатор стены (SWS/SWW/WWS/WWW = 0..3) по соседям слева/справа.
|
||||
seg008.c:1255-1305 (без USE_FAKE_TILES): сосед считается стеной, если
|
||||
его тип == wall(20). На кромке комнаты без линка сосед по умолчанию
|
||||
считается стеной (wall_to_left/right = 1)."""
|
||||
left_code = room.tile(drawn_row, col - 1)[0] if col > 0 else 20
|
||||
right_code = room.tile(drawn_row, col + 1)[0] if col < 9 else 20
|
||||
wall_left = (left_code == 20)
|
||||
wall_right = (right_code == 20)
|
||||
if wall_left and wall_right:
|
||||
return WALL_MODIFIER_WWW # 3
|
||||
if wall_left:
|
||||
return WALL_MODIFIER_WWS # 2
|
||||
if wall_right:
|
||||
return WALL_MODIFIER_SWW # 1
|
||||
return WALL_MODIFIER_SWS # 0
|
||||
|
||||
|
||||
def _wall_add(canvas, which_table, id, xh, xl, ybottom, reason):
|
||||
"""Emulate ptr_add_table(RSET_WALL, id, xh, xl, ybottom, blit, 0).
|
||||
which_table 0 -> backtable (blit transparent), 1 -> foretable.
|
||||
RSET_WALL id -> res(200+id).png, cascade VDUNGEON->VPALACE."""
|
||||
img = load_sprite(id, CHTAB_WALL, reason=reason)
|
||||
if img is None:
|
||||
return
|
||||
y = ybottom - img.height + 1
|
||||
x = xh * 8 + xl
|
||||
blit(canvas, img, x, y)
|
||||
|
||||
|
||||
def draw_left_mark(canvas, decal_variant, arg2, arg1, drawn_row, col):
|
||||
image_id = RES_WALL_MARK_TL
|
||||
if decal_variant % 2:
|
||||
image_id = RES_WALL_MARK_BL
|
||||
lv2 = 0
|
||||
if decal_variant > 3:
|
||||
lv2 = arg1 + 6
|
||||
elif decal_variant > 1:
|
||||
lv2 = arg2 + 6
|
||||
xh = COL_XH[col]
|
||||
draw_bottom_y = 63 * drawn_row + 65
|
||||
_wall_add(canvas, 1, image_id, xh + (1 if decal_variant in (2, 3) else 0),
|
||||
lv2, draw_bottom_y - LPOS[decal_variant],
|
||||
reason=f"wall left-mark var{decal_variant}")
|
||||
|
||||
|
||||
def draw_right_mark(canvas, decal_variant, arg1, drawn_row, col):
|
||||
image_id = RES_WALL_MARK_TR
|
||||
if decal_variant % 2:
|
||||
image_id = RES_WALL_MARK_BR
|
||||
a = 24 if decal_variant < 2 else arg1 - 3
|
||||
xh = COL_XH[col]
|
||||
draw_bottom_y = 63 * drawn_row + 65
|
||||
_wall_add(canvas, 1, image_id, xh + (1 if decal_variant > 1 else 0),
|
||||
a, draw_bottom_y - RPOS[decal_variant],
|
||||
reason=f"wall right-mark var{decal_variant}")
|
||||
|
||||
|
||||
def wall_pattern(canvas, room, drawn_row, col, which_part, which_table):
|
||||
"""Replay seg008.c wall_pattern() for a dungeon wall tile.
|
||||
which_part: 0 = bottom part (draw_tile_bottom), 1 = fore part (draw_tile_fore).
|
||||
which_table: 0 = backtable, 1 = foretable."""
|
||||
global _random_seed
|
||||
xh = COL_XH[col]
|
||||
draw_bottom_y = 63 * drawn_row + 65
|
||||
draw_main_y = draw_bottom_y - 3
|
||||
|
||||
saved_seed = _random_seed
|
||||
# seg008.c:1941 — зерно зависит от НОМЕРА КОМНАТЫ (не строки!):
|
||||
# random_seed = drawn_room + tbl_line[drawn_row] + drawn_col
|
||||
room_num = getattr(room, "room_num", 1)
|
||||
_random_seed = room_num + TBL_LINE[drawn_row] + col
|
||||
prandom(1) # discard
|
||||
is_dungeon = IS_DUNGEON
|
||||
|
||||
middle_divider = prandom(1)
|
||||
middle_divider_offset = prandom(4)
|
||||
bottom_divider = prandom(1)
|
||||
bottom_divider_offset = prandom(4)
|
||||
|
||||
# Модификатор стены (какие соседи — стены) — тот же, что для сплошной грани.
|
||||
bg = wall_modifier(room, drawn_row, col)
|
||||
|
||||
if bg == WALL_MODIFIER_WWW:
|
||||
if which_part != 0:
|
||||
if prandom(4) == 0:
|
||||
_wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0,
|
||||
draw_bottom_y - 42, reason="wall rndblock")
|
||||
_wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1,
|
||||
middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider")
|
||||
_wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh,
|
||||
bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider")
|
||||
if which_part != 0 and is_dungeon:
|
||||
if prandom(4) == 0:
|
||||
draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col)
|
||||
if prandom(4) == 0:
|
||||
draw_left_mark(canvas, prandom(4),
|
||||
middle_divider_offset - middle_divider,
|
||||
bottom_divider_offset - bottom_divider, drawn_row, col)
|
||||
elif bg == WALL_MODIFIER_SWS:
|
||||
if is_dungeon and which_part != 0:
|
||||
if prandom(6) == 0:
|
||||
draw_left_mark(canvas, prandom(1),
|
||||
middle_divider_offset - middle_divider,
|
||||
bottom_divider_offset - bottom_divider, drawn_row, col)
|
||||
elif bg == WALL_MODIFIER_SWW:
|
||||
if which_part != 0:
|
||||
if prandom(4) == 0:
|
||||
_wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0,
|
||||
draw_bottom_y - 42, reason="wall rndblock")
|
||||
_wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1,
|
||||
middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider")
|
||||
if is_dungeon:
|
||||
if prandom(4) == 0:
|
||||
draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col)
|
||||
if prandom(4) == 0:
|
||||
draw_left_mark(canvas, prandom(3),
|
||||
middle_divider_offset - middle_divider,
|
||||
bottom_divider_offset - bottom_divider, drawn_row, col)
|
||||
elif bg == WALL_MODIFIER_WWS:
|
||||
if which_part != 0:
|
||||
_wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1,
|
||||
middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider")
|
||||
_wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh,
|
||||
bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider")
|
||||
if which_part != 0 and is_dungeon:
|
||||
if prandom(4) == 0:
|
||||
draw_right_mark(canvas, prandom(1) + 2, middle_divider_offset, drawn_row, col)
|
||||
if prandom(4) == 0:
|
||||
draw_left_mark(canvas, prandom(4),
|
||||
middle_divider_offset - middle_divider,
|
||||
bottom_divider_offset - bottom_divider, drawn_row, col)
|
||||
|
||||
_random_seed = saved_seed
|
||||
|
||||
|
||||
# Per-tile sprite log: draw_tile resets CURRENT_TILE_LOG, load_sprite appends to it.
|
||||
CURRENT_TILE_LOG = []
|
||||
TILE_LOGS = {} # (row, col) -> list of (id, source, reason)
|
||||
|
||||
# seg008.c:1108 — вертикальные слайсы решётки ворот (portcullis).
|
||||
DOOR_FRAM_SLICE = [67, 59, 58, 57, 56, 55, 54, 53, 52]
|
||||
# seg008.c:517 — правый край шипов по кадру get_spike_frame.
|
||||
SPIKES_FRAM_RIGHT = [0, 134, 135, 136, 137, 138, 137, 135, 134, 0]
|
||||
# seg008.c:450 — «blueline»-декали на ЗАДНЕЙ стене (силуэты кладки/окна),
|
||||
# рисуются правым соседом по МОДИФИКАТОРУ соседа слева:
|
||||
# tile_left == empty -> blueline_fram1[mod] (окно/решётка-фрагменты 124..126)
|
||||
# на высоте blueline_fram_y[mod] + main_y;
|
||||
# tile_left == floor -> blueline_fram3[mod] (кирпичные силуэты 44/45)
|
||||
# на main_y - 20 (если mod != !!level_type).
|
||||
BLUELINE_FRAM1 = [0, 124, 125, 126]
|
||||
BLUELINE_FRAM_Y = [0, -20, -20, 0]
|
||||
BLUELINE_FRAM3 = [44, 44, 45, 45]
|
||||
|
||||
|
||||
def draw_gate_back(canvas, modifier_left, draw_xh, draw_bottom_y, draw_main_y):
|
||||
"""seg008.c:1110 draw_gate_back — портикулис (решётка) ворот. Рисуется
|
||||
на тайле СПРАВА от ворот (draw_tile_anim_right, tile_left==gate). У нас
|
||||
это col 0 room1, где сосед слева — ворота из room 5. modifier_left —
|
||||
модификатор тайла-ворот (степень поднятия решётки)."""
|
||||
x = draw_xh * 8
|
||||
gate_top_y = draw_bottom_y - 62
|
||||
gate_openness = (min(modifier_left, 188) >> 2) + 1
|
||||
gate_bottom_y = draw_main_y - gate_openness
|
||||
if gate_bottom_y + 12 < draw_main_y:
|
||||
img = load_sprite(50, reason="gate bottom+B") # gate поднята высоко
|
||||
if img:
|
||||
blit(canvas, img, x, gate_bottom_y - img.height + 1)
|
||||
else:
|
||||
# id 47 (правая грань ворот) уже рисует draw_tile_right — не дублируем.
|
||||
img = load_sprite(51, reason="gate bottom")
|
||||
if img:
|
||||
blit(canvas, img, x, (gate_bottom_y - 2) - img.height + 1)
|
||||
# Стек 8px-слайсов решётки снизу вверх.
|
||||
ybottom = gate_bottom_y - 12
|
||||
if ybottom < 192:
|
||||
while ybottom >= 0 and ybottom > 7 and (ybottom - 7) > gate_top_y:
|
||||
img = load_sprite(52, reason="gate slice 8px")
|
||||
if img:
|
||||
blit(canvas, img, x, ybottom - img.height + 1)
|
||||
ybottom -= 8
|
||||
gate_frame = ybottom - gate_top_y + 1
|
||||
if 0 < gate_frame < 9:
|
||||
img = load_sprite(DOOR_FRAM_SLICE[gate_frame], reason="gate top slice")
|
||||
if img:
|
||||
blit(canvas, img, x, ybottom - img.height + 1)
|
||||
|
||||
|
||||
def draw_tile(canvas, room, drawn_row, col, left_edge_code=20):
|
||||
global CURRENT_TILE_LOG
|
||||
CURRENT_TILE_LOG = []
|
||||
code, fg_byte, modbit, bg_mod = room.tile(drawn_row, col)
|
||||
# tile_left: колонка слева. col-1 == -1 -> Room.tile берёт из левой
|
||||
# комнаты (или уровневая кромка = стена), см. Room.tile / load_leftroom.
|
||||
left_code, _, _, left_mod = room.tile(drawn_row, col - 1)
|
||||
draw_xh = COL_XH[col]
|
||||
x = draw_xh * 8
|
||||
draw_bottom_y = 63 * drawn_row + 65
|
||||
draw_main_y = draw_bottom_y - 3
|
||||
|
||||
# For topright/floorright we need the tile below-left.
|
||||
# Room below = same room (no room links) -> row_below = drawn_row+1 if <3 else treat as empty/floor top edge.
|
||||
row_below = drawn_row + 1
|
||||
if row_below > 2:
|
||||
# below the bottom row of the level: floor top edge
|
||||
rbl_code, rbl_mod = 1, 0
|
||||
else:
|
||||
rbl_code, _, _, rbl_mod = room.tile(row_below, col - 1)
|
||||
row_below_left_code = rbl_code
|
||||
|
||||
t = TILE_TABLE[code]
|
||||
lt = TILE_TABLE[left_code]
|
||||
|
||||
# ---- draw_tile_floorright (seg008.c:392) ----
|
||||
# Рисуется ТОЛЬКО когда can_see_bottomleft(): текущий тайл —
|
||||
# empty/bigpillar_top/doortop/lattice_down. Иначе topright соседа
|
||||
# снизу-слева перекрыт и рисовать его нельзя (иначе «арки» повсюду —
|
||||
# это была главная причина замусоренного рендера).
|
||||
if code in (0, 9, 12, 26): # can_see_bottomleft
|
||||
_draw_topright(canvas, row_below_left_code, rbl_mod, x, draw_bottom_y,
|
||||
reason=f"topright(below-left tile {row_below_left_code:02X})")
|
||||
if lt[4]: # tile_table[tile_left].floor_right
|
||||
# blitters_9_black: ЧЁРНЫЙ силуэт (тень уступа), не светлый спрайт.
|
||||
img = load_sprite(42, reason=f"floor-right shadow(left tile {left_code:02X})")
|
||||
blit_black(canvas, img, x, draw_main_y + TILE_TABLE[1][5] - (img.height if img else 0) + 1)
|
||||
|
||||
# ---- draw_tile_anim_topright ---- (gate top mask) - skipped: no anim
|
||||
# ---- draw_tile_right (seg008.c:456) ----
|
||||
if code != 20: # у самой стены правый край не рисуется
|
||||
level_type = 0 if IS_DUNGEON else 1
|
||||
if left_code == 20:
|
||||
# сосед слева — стена: её правая грань в текущую колонку (chtab_7 id 1)
|
||||
img = load_sprite(1, CHTAB_WALL, reason="wall right-face(left tile 14)")
|
||||
ry = TILE_TABLE[20][5] # right_y стены = 2
|
||||
blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1)
|
||||
elif left_code == 0:
|
||||
# сосед слева — empty: фрагмент окна/решётки по его модификатору
|
||||
if left_mod <= 3:
|
||||
bid = BLUELINE_FRAM1[left_mod]
|
||||
if bid:
|
||||
img = load_sprite(bid, reason=f"blueline-window(left empty mod{left_mod})")
|
||||
blit(canvas, img, x,
|
||||
BLUELINE_FRAM_Y[left_mod] + draw_main_y - (img.height if img else 0) + 1)
|
||||
elif left_code == 1:
|
||||
# сосед слева — floor: правый треугольник (42) + СИЛУЭТ КЛАДКИ (44/45)
|
||||
# на задней стене, если модификатор пола != !!level_type.
|
||||
img = load_sprite(42, reason="floor-right(part of left tile 01)")
|
||||
ry = TILE_TABLE[1][5] # right_y пола = 2
|
||||
blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1)
|
||||
num = left_mod if left_mod <= 3 else 0
|
||||
if num != level_type:
|
||||
bid = BLUELINE_FRAM3[num]
|
||||
img = load_sprite(bid, reason=f"brick-silhouette(left floor mod{num})")
|
||||
blit(canvas, img, x, draw_main_y - 20 - (img.height if img else 0) + 1)
|
||||
else:
|
||||
rid = lt[3] # tile_left.right_id
|
||||
if rid:
|
||||
ry = lt[5]
|
||||
img = load_sprite(rid, reason=f"right(part of left tile {left_code:02X})")
|
||||
blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1)
|
||||
# stripe: only drawn when tbl_level_type != 0 (palace). Dungeon: skip.
|
||||
sid = lt[6]
|
||||
if sid and not IS_DUNGEON:
|
||||
img = load_sprite(sid, reason=f"stripe(left tile {left_code:02X})")
|
||||
blit(canvas, img, x, draw_main_y - 27 - (img.height if img else 0) + 1)
|
||||
|
||||
# ---- draw_tile_anim_right (seg008.c:530) ----
|
||||
# «Анимированные» части соседа слева, проецируемые в текущий тайл.
|
||||
# Статикой (кадр покоя) рисуем те, что формируют ГЕОМЕТРИЮ фона:
|
||||
# gate -> решётка (portcullis) на границе с левой комнатой;
|
||||
# loose -> правый треугольный край шаткой плиты (иначе провал в полу);
|
||||
# spike -> статичный кадр шипов по модификатору.
|
||||
# torch-flame/chomper — чистая анимация, пропускаем (отдельные sprite_t).
|
||||
if left_code == 4: # tiles_4_gate
|
||||
draw_gate_back(canvas, left_mod, draw_xh, draw_bottom_y, draw_main_y)
|
||||
elif left_code == 11: # tiles_11_loose — правый край плиты (loose_fram_right[0]=42)
|
||||
img = load_sprite(42, reason="loose right (part of left tile 0B)")
|
||||
if img:
|
||||
blit(canvas, img, x, (draw_bottom_y - 1) - img.height + 1)
|
||||
elif left_code == 2: # tiles_2_spike — статичный кадр (spikes_fram_right)
|
||||
sf = 5 if (left_mod & 0x80) else left_mod
|
||||
sid = SPIKES_FRAM_RIGHT[sf] if 0 <= sf < len(SPIKES_FRAM_RIGHT) else 0
|
||||
if sid:
|
||||
img = load_sprite(sid, reason="spike right (part of left tile 02)")
|
||||
if img:
|
||||
blit(canvas, img, x, (draw_main_y - 7) - img.height + 1)
|
||||
|
||||
# ---- draw_tile_bottom (seg008.c:570) ----
|
||||
if code == 20:
|
||||
# Сплошная НИЖНЯЯ грань стены (chtab_7 wall_fram_bottom) + декали.
|
||||
wmod = wall_modifier(room, drawn_row, col)
|
||||
img = load_sprite(WALL_FRAM_BOTTOM[wmod], CHTAB_WALL, reason="wall bottom-face")
|
||||
if img:
|
||||
blit(canvas, img, x, draw_bottom_y - img.height + 1)
|
||||
wall_pattern(canvas, room, drawn_row, col, 0, 0)
|
||||
else:
|
||||
bid = t[8]
|
||||
img = load_sprite(bid, reason=f"bottom({TILE_NAMES.get(code,'?')})")
|
||||
if img:
|
||||
blit(canvas, img, x, draw_bottom_y - img.height + 1)
|
||||
|
||||
# ---- draw_loose (seg008.c:600) — статический кадр loose floor ----
|
||||
# Нижняя часть шаткой плиты (loose_fram_bottom[0]=43) на draw_bottom_y.
|
||||
if code == 11: # 0x0B loose floor
|
||||
img = load_sprite(43, reason="loose bottom (draw_loose)")
|
||||
if img:
|
||||
blit(canvas, img, x, draw_bottom_y - img.height + 1)
|
||||
|
||||
# ---- draw_tile_base (seg008.c:611) ----
|
||||
base_id = t[0]
|
||||
base_y = t[2]
|
||||
if code == 11: # loose floor: базовая (верхняя) часть = loose_fram_left[0] = 41
|
||||
base_id = 41
|
||||
img = load_sprite(base_id, reason=f"base({TILE_NAMES.get(code,'?')})")
|
||||
if img:
|
||||
blit(canvas, img, x, base_y + draw_main_y - img.height + 1)
|
||||
|
||||
# ---- draw_tile_anim ---- (spike/potion/chomper animated) skipped
|
||||
|
||||
# ---- draw_tile_fore (seg008.c:690) ----
|
||||
if code == 20:
|
||||
# Основная (средняя) грань стены (chtab_7 wall_fram_main) + декали/марки.
|
||||
wmod = wall_modifier(room, drawn_row, col)
|
||||
img = load_sprite(WALL_FRAM_MAIN[wmod], CHTAB_WALL, reason="wall main-face")
|
||||
if img:
|
||||
blit(canvas, img, x, draw_main_y - img.height + 1)
|
||||
wall_pattern(canvas, room, drawn_row, col, 1, 1)
|
||||
fid = t[9]
|
||||
if fid:
|
||||
# fore_x — в единицах xh (×8 пикс!), не в пикселях: seg008.c:743
|
||||
# xh = fore_x + draw_xh, затем x = xh*8. fore_y — в пикселях.
|
||||
fx = t[10] * 8 + x
|
||||
fy = t[11] + draw_main_y
|
||||
img = load_sprite(fid, reason=f"fore({TILE_NAMES.get(code,'?')})")
|
||||
if img:
|
||||
blit(canvas, img, fx, fy - img.height + 1)
|
||||
|
||||
# Save per-tile log (keyed by logical room position: row=drawn_row, col)
|
||||
TILE_LOGS[(drawn_row, col)] = list(CURRENT_TILE_LOG)
|
||||
|
||||
|
||||
def _draw_topright(canvas, tiletype, modifier, x, draw_bottom_y, reason=""):
|
||||
if tiletype in (7, 12): # doortop_with_floor / doortop -> palace uses frame
|
||||
return
|
||||
elif tiletype == 20: # wall
|
||||
img = load_sprite(2, CHTAB_WALL, reason=reason)
|
||||
else:
|
||||
tid = TILE_TABLE[tiletype][7]
|
||||
if tid == 0:
|
||||
return
|
||||
img = load_sprite(tid, reason=reason)
|
||||
if img:
|
||||
blit(canvas, img, x, draw_bottom_y - img.height + 1)
|
||||
|
||||
|
||||
def render_room(room_num):
|
||||
with open(os.path.join(LEVEL_DIR, f"res{2000 + room_num}.bin"), "rb") as f:
|
||||
data = f.read()
|
||||
level = Level(data)
|
||||
room = Room(level, room_num)
|
||||
# load_leftroom(): левая комната по линку L — её col 9 виден на границе
|
||||
# (col 0) текущей комнаты (напр. решётка ворот из соседней комнаты).
|
||||
left_link = level.links[room_num][0]
|
||||
if left_link:
|
||||
room.left_room = Room(level, left_link)
|
||||
|
||||
canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0))
|
||||
# rows drawn 2,1,0 (bottom first)
|
||||
for drawn_row in (2, 1, 0):
|
||||
for col in range(10):
|
||||
draw_tile(canvas, room, drawn_row, col)
|
||||
|
||||
return canvas, room, level
|
||||
|
||||
|
||||
def render_single_tile(code, modifier=0, left_code=20, below_left_code=1):
|
||||
"""Render one tile in isolation on a 40x64 canvas (matching SDLPoP tile box)."""
|
||||
TILE_W, TILE_H = 40, 64
|
||||
canvas = Image.new("RGBA", (TILE_W, TILE_H), (0, 0, 0, 0))
|
||||
# Place tile as if it were at drawn_row=2 (bottom), col=0
|
||||
drawn_row = 2
|
||||
col = 0
|
||||
# Build a fake Room backed by single values
|
||||
class FakeRoom:
|
||||
def tile(self, r, c):
|
||||
if c < 0:
|
||||
return left_code, 0, 0, 0
|
||||
if r < 0:
|
||||
return 1, 0, 0, 0
|
||||
if r == drawn_row and c == 0:
|
||||
return code, 0, 0, modifier
|
||||
return below_left_code, 0, 0, 0
|
||||
room = FakeRoom()
|
||||
draw_tile(canvas, room, drawn_row, col, left_edge_code=left_code)
|
||||
return canvas
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
room_num = 1
|
||||
if len(sys.argv) > 1:
|
||||
room_num = int(sys.argv[1])
|
||||
|
||||
canvas, room, level = render_room(room_num)
|
||||
out = f"./room{room_num}_render.png"
|
||||
# Прозрачный фон (задняя чёрная стена, которую мы не рисуем) -> ЧЁРНЫМ,
|
||||
# а не белым как показывает вьюер прозрачность. Композитим по альфе.
|
||||
flat = Image.new("RGB", canvas.size, (0, 0, 0))
|
||||
flat.paste(canvas, (0, 0), canvas)
|
||||
flat.save(out)
|
||||
print(f"Saved {out} ({canvas.width}x{canvas.height})")
|
||||
|
||||
# debug grid with code labels (на чёрном фоне)
|
||||
from PIL import ImageDraw
|
||||
dbg = flat.copy()
|
||||
d = ImageDraw.Draw(dbg)
|
||||
for r in range(3):
|
||||
by = 63 * r + 65
|
||||
for c in range(10):
|
||||
x = COL_XH[c] * 8
|
||||
d.rectangle([x, by - 63, x + 31, by], outline=(255, 0, 0), width=1)
|
||||
code, _, _, _ = room.tile(r, c)
|
||||
d.text((x + 2, by - 60), f"{code:02X}", fill=(255, 255, 0))
|
||||
dbg.save(f"./room{room_num}_debug.png")
|
||||
print(f"Saved room{room_num}_debug.png")
|
||||
|
||||
# Per-tile individual renders (the 30 tiles of the room)
|
||||
# For proper neighbour context, render each tile inside its real room,
|
||||
# then crop its 32x64 (40x64) column box.
|
||||
tiles_dir = f"./tiles_room{room_num}"
|
||||
os.makedirs(tiles_dir, exist_ok=True)
|
||||
for r in range(3):
|
||||
by = 63 * r + 65
|
||||
for c in range(10):
|
||||
x = COL_XH[c] * 8
|
||||
code, _, _, _ = room.tile(r, c)
|
||||
# crop the column box (40 wide to include left overlap, 64 tall above bottom)
|
||||
box = (x, by - 64, x + 40, by)
|
||||
tile_img = canvas.crop(box)
|
||||
tile_img.save(os.path.join(tiles_dir, f"r{r}c{c}_{code:02X}.png"))
|
||||
print(f"Saved 30 tile crops to {tiles_dir}/")
|
||||
# Per-tile report: which images each tile pulls and why.
|
||||
print()
|
||||
print("=" * 78)
|
||||
print(f"PER-TILE SPRITE REPORT (room {room_num}, {'DUNGEON' if IS_DUNGEON else 'PALACE'})")
|
||||
print("=" * 78)
|
||||
for r in range(3):
|
||||
for c in range(10):
|
||||
code, fg_byte, mbit, bb = room.tile(r, c)
|
||||
name = TILE_NAMES.get(code, f"?0x{code:02X}")
|
||||
btd = level.backtable_desc(code, bb)
|
||||
line = (f"\n[{r},{c}] code=0x{code:02X} ({name}) "
|
||||
f"fg=0x{fg_byte:02X} mbit={mbit} backtable=0x{bb:02X} ({btd})")
|
||||
# If this tile is an event activator (group 'event'), show the door it drives.
|
||||
if code in (0x06, 0x0F):
|
||||
scr, loc, trig = level.door_for_event(bb)
|
||||
line += f"\n -> DOOR EVENT id={bb}: screen={scr} loc={loc} trigger_next={trig}"
|
||||
print(line)
|
||||
logs = TILE_LOGS.get((r, c), [])
|
||||
if not logs:
|
||||
print(" (no sprites drawn)")
|
||||
for sid, source, reason in logs:
|
||||
print(f" res{200+sid:03d}.png <- {source:8s} {reason}")
|
||||
print()
|
||||
print("=" * 78)
|
||||
|
||||
# Level-level summary: start position, links, door events, guards.
|
||||
print()
|
||||
print("LEVEL STRUCTURE SUMMARY")
|
||||
print("-" * 78)
|
||||
print(f"start position: room={level.start[0]} loc={level.start[1]} dir={level.start[2]}")
|
||||
links = level.links.get(room_num, (0, 0, 0, 0))
|
||||
print(f"room {room_num} links: L={links[0]} R={links[1]} Up={links[2]} Dn={links[3]}")
|
||||
# guard in this room
|
||||
gi = room_num - 1
|
||||
gloc = level.guard_loc[gi]
|
||||
if gloc < 30:
|
||||
gr, gc = gloc // 10, gloc % 10
|
||||
print(f"guard: loc=tile(r{gr},c{gc}) dir={level.guard_dir[gi]} "
|
||||
f"skill={level.guard_skill[gi]} colour={level.guard_colour[gi]}")
|
||||
else:
|
||||
print("guard: none")
|
||||
# door events driven by any tile in this room
|
||||
print("door events referenced by this room's tiles:")
|
||||
seen = set()
|
||||
for off in range(30):
|
||||
fb, code, mbit = room.fg[off]
|
||||
bb = room.bg[off]
|
||||
if code in (0x06, 0x0F):
|
||||
if bb not in seen:
|
||||
seen.add(bb)
|
||||
scr, loc, trig = level.door_for_event(bb)
|
||||
print(f" event id={bb} (tile off {off}): screen={scr} loc={loc} trigger_next={trig}")
|
||||
if not seen:
|
||||
print(" (none)")
|
||||
|
||||
print()
|
||||
print("Room", room_num, "foreground codes (3 rows x 10 cols):")
|
||||
for r in range(3):
|
||||
print(" ", " ".join(f"{room.tile(r,c)[0]:02X}" for c in range(10)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 543 B |
|
After Width: | Height: | Size: 89 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 216 B |
|
After Width: | Height: | Size: 239 B |
|
After Width: | Height: | Size: 301 B |
|
After Width: | Height: | Size: 239 B |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 595 B |
|
After Width: | Height: | Size: 595 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 628 B |
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 595 B |
|
After Width: | Height: | Size: 651 B |
|
After Width: | Height: | Size: 597 B |
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 588 B |
|
After Width: | Height: | Size: 607 B |
|
After Width: | Height: | Size: 646 B |
|
After Width: | Height: | Size: 712 B |
|
After Width: | Height: | Size: 844 B |
|
After Width: | Height: | Size: 767 B |
|
After Width: | Height: | Size: 458 B |
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 541 B |