c127a4b0d2
Графика chtab_1 (flame/sword/potion) — новый атлас pop_pot.atl: - склянка зелья берётся из chtab_1 (seg008 draw_tile_fore: id 12 малая / 13 большая при типах 2..4), а НЕ chtab_6 id 12 — res212 в VDUNGEON нет, каскад падал на VPALACE и рисовал кусок дворцовой декорации («мусорный объект» в комнате 5); - пакуем chtab_1 целиком (id 1..23): пламя, склянки, кадры пузырька; - MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из ОБЩЕЙ 16-цветной палитры экрана — она добавлена в 0x30..0x3F, палитра chtab_1 в 0x40..0x4F; пузырёк пакуется силуэтом цвета 12 (красный), его маска — цвета 0; - в env-атлас добавлено основание факела (env 146, seg008:489 — рисует правый сосед); в статический render_room оно не попадало. Анимация (seg007 animate_torch/animate_potion + seg000:0B12 anim_tile_modif): при входе в комнату факелам и зельям задаётся случайная стартовая фаза и заводится trob; факел — get_torch_frame, пламя в ячейке ПРАВОГО соседа (xh = draw_xh+1, y = draw_main_y−40); зелье — bubble_next_frame по младшим 3 битам модификатора (старшие 5 = тип, при загрузке уровня modif <<= 3, seg009). Пламя и пузырёк не запекаются в фон: heal своей области + кадр поверх. Скорость пламени /2 — наш логический кадр короче игрового тика оригинала. Скорость отрисовки (профиль в MAME, кадр = 430 080 тактов): - замер показал, что цена блита почти НЕ зависит от размера — 13 288 тактов на спрайт 32×3 против 4 617 у линейного спрайтового ядра без клипа; платим за проход gfx_blit → gfx_blit_part → _gfx_blit_full; - в libbgi добавлен gfx_blit_noclip() — блит без клипа в ТЕКУЩЕМ банке (putsprite не годится: навязывает GFX_BANK_SPRITE, фону нужен TRANSPARENT ради ОЗУ-копии); pop_bg.blit_b уходит на него, когда спрайт целиком на экране и не нужен g_clip_top → ~2.9× на блит; - W3-скобку ставит САМА libbgi: из модуля с --w3 её вызывать нельзя — после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает (проявлялось белым экраном); - редрой шва больше не перерисовывает полосу потолка (bar начинается с POP_YOFF+3) — это удваивало стоимость блока при анимации решётки; - docs/size_optimization_plan.md §7–§8: замеры, сделанное и запас (батчинг W3-скобки, решётка одним спрайтом, лишние блиты). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
6.2 KiB
Python
144 lines
6.2 KiB
Python
#!/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 обязаны жить в одном файле): chtab_1@0x40 (пламя/зелья),
|
|
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
|
|
(B.POT_PAL_BASE, os.path.join(os.path.dirname(B.R.VDUNGEON_DIR),
|
|
B.POT_DIR_NAME, "res150.pal")), # chtab_1 0x40
|
|
]
|
|
# 16 базовых цветов экрана (mono-блит красит ими: пламя, пузырьки зелья)
|
|
for i, (r, g, bl) in enumerate(B.VGA16):
|
|
o = (B.VGA16_PAL_BASE + i) * 4
|
|
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
|
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()
|