diff --git a/applications/PoP/docs/shadow_atlas_plan.md b/applications/PoP/docs/shadow_atlas_plan.md index c4a12a5..59d97fa 100644 --- a/applications/PoP/docs/shadow_atlas_plan.md +++ b/applications/PoP/docs/shadow_atlas_plan.md @@ -1,6 +1,7 @@ # Атлас Тени — разбор и план -Дата: 2026-08-20. Статус: **разбор сделан, код не писали.** +Дата: 2026-08-20. Статус: **СДЕЛАНО** (Ш0-Ш3), проверено на 6-м уровне; +уровни 4 и 12 ещё не прогонялись. Задача: сейчас Тень рисуется спрайтами Кида (вне боя) и спрайтами стража (в бою) как есть, поэтому от Кида она не отличается. В оригинале её вид @@ -225,3 +226,39 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре `curr_guard_color` у не-стражей равен 0 (`seg002:183`). Ветку `pop_guard_set_palette` для типа 4 звать нельзя — она затрёт палитру Тени палитрой стража. + +--- + +# 8. Как сделали (2026-08-20) + +- `toolchain/pop_pack_shadow.py` — запекает обе половины, собирает + 16-цветную палитру гибридом (получилось 5 частых точно + 11 кластеров + хвоста), пишет `poc/res/shadow/sk0..27.atl`, `sf0..3.atl` и + `roomtest/pop_shadow_atlas.h`. Итог: **251 спрайт, 32 EMM-страницы, + 225 178 Б**. +- `roomtest/pop_shadow.c/.h` (банк 8) — загрузка. **Грузим один раз при + старте**, а не по уровням: страниц EMM с запасом, а забыть перезагрузку + на границе легко — ровно так и появился BUG-SHADOW-SET. +- `pop_cdraw.c` — одна ветка выбора атласа; заодно брызги урона теперь + берут атлас отдельным указателем (`spl`), потому что у оригинала они + всегда из «своего» chtab, а у Тени кадр может прийти из другой + половины набора. + +## Грабля, стоившая одного прогона + +Набор загрузился, силуэт нарисовался правильной формы — и **целиком +чёрный**. Причина: `gfx_pal_fload("KID\\kid.pal")` заливает ВСЕ 256 +записей палитры и затирает любые слоты, выставленные до него. Ровно та +же беда уже была с тайлсетом — сразу за этим вызовом стоит +`pop_bg_pal_apply`. Поэтому палитра Тени вынесена в отдельный +`pop_shadow_pal_apply()` и зовётся там же, а не внутри загрузки атласов. + +**Правило на будущее: любой новый набор палитровых слотов красится ПОСЛЕ +kid.pal, рядом с pop_bg_pal_apply.** + +## Что осталось + +- прогнать уровень 4 (рождение из зеркала) и 12 (бой — там работает + вторая половина набора, `sf*`); +- записать в `impl_diff.md` расхождение из §2 (кайма в один пиксель по + левым кромкам на НЕчёрном фоне). diff --git a/applications/PoP/poc/res/shadow/sf0.atl b/applications/PoP/poc/res/shadow/sf0.atl new file mode 100644 index 0000000..313de95 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sf0.atl differ diff --git a/applications/PoP/poc/res/shadow/sf1.atl b/applications/PoP/poc/res/shadow/sf1.atl new file mode 100644 index 0000000..daccba6 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sf1.atl differ diff --git a/applications/PoP/poc/res/shadow/sf2.atl b/applications/PoP/poc/res/shadow/sf2.atl new file mode 100644 index 0000000..1f0b6d4 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sf2.atl differ diff --git a/applications/PoP/poc/res/shadow/sf3.atl b/applications/PoP/poc/res/shadow/sf3.atl new file mode 100644 index 0000000..5c54e5a Binary files /dev/null and b/applications/PoP/poc/res/shadow/sf3.atl differ diff --git a/applications/PoP/poc/res/shadow/sk0.atl b/applications/PoP/poc/res/shadow/sk0.atl new file mode 100644 index 0000000..00d2e35 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk0.atl differ diff --git a/applications/PoP/poc/res/shadow/sk1.atl b/applications/PoP/poc/res/shadow/sk1.atl new file mode 100644 index 0000000..4089c79 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk1.atl differ diff --git a/applications/PoP/poc/res/shadow/sk10.atl b/applications/PoP/poc/res/shadow/sk10.atl new file mode 100644 index 0000000..943fcf7 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk10.atl differ diff --git a/applications/PoP/poc/res/shadow/sk11.atl b/applications/PoP/poc/res/shadow/sk11.atl new file mode 100644 index 0000000..c573db6 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk11.atl differ diff --git a/applications/PoP/poc/res/shadow/sk12.atl b/applications/PoP/poc/res/shadow/sk12.atl new file mode 100644 index 0000000..c4be0c0 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk12.atl differ diff --git a/applications/PoP/poc/res/shadow/sk13.atl b/applications/PoP/poc/res/shadow/sk13.atl new file mode 100644 index 0000000..eb433b5 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk13.atl differ diff --git a/applications/PoP/poc/res/shadow/sk14.atl b/applications/PoP/poc/res/shadow/sk14.atl new file mode 100644 index 0000000..f07e418 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk14.atl differ diff --git a/applications/PoP/poc/res/shadow/sk15.atl b/applications/PoP/poc/res/shadow/sk15.atl new file mode 100644 index 0000000..80217bb Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk15.atl differ diff --git a/applications/PoP/poc/res/shadow/sk16.atl b/applications/PoP/poc/res/shadow/sk16.atl new file mode 100644 index 0000000..ee4bd35 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk16.atl differ diff --git a/applications/PoP/poc/res/shadow/sk17.atl b/applications/PoP/poc/res/shadow/sk17.atl new file mode 100644 index 0000000..86e63a6 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk17.atl differ diff --git a/applications/PoP/poc/res/shadow/sk18.atl b/applications/PoP/poc/res/shadow/sk18.atl new file mode 100644 index 0000000..07498ba Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk18.atl differ diff --git a/applications/PoP/poc/res/shadow/sk19.atl b/applications/PoP/poc/res/shadow/sk19.atl new file mode 100644 index 0000000..24a06d3 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk19.atl differ diff --git a/applications/PoP/poc/res/shadow/sk2.atl b/applications/PoP/poc/res/shadow/sk2.atl new file mode 100644 index 0000000..c4e5e72 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk2.atl differ diff --git a/applications/PoP/poc/res/shadow/sk20.atl b/applications/PoP/poc/res/shadow/sk20.atl new file mode 100644 index 0000000..6b1e0f7 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk20.atl differ diff --git a/applications/PoP/poc/res/shadow/sk21.atl b/applications/PoP/poc/res/shadow/sk21.atl new file mode 100644 index 0000000..d446b37 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk21.atl differ diff --git a/applications/PoP/poc/res/shadow/sk22.atl b/applications/PoP/poc/res/shadow/sk22.atl new file mode 100644 index 0000000..ae853da Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk22.atl differ diff --git a/applications/PoP/poc/res/shadow/sk23.atl b/applications/PoP/poc/res/shadow/sk23.atl new file mode 100644 index 0000000..450828b Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk23.atl differ diff --git a/applications/PoP/poc/res/shadow/sk24.atl b/applications/PoP/poc/res/shadow/sk24.atl new file mode 100644 index 0000000..258ba93 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk24.atl differ diff --git a/applications/PoP/poc/res/shadow/sk25.atl b/applications/PoP/poc/res/shadow/sk25.atl new file mode 100644 index 0000000..8d3c861 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk25.atl differ diff --git a/applications/PoP/poc/res/shadow/sk26.atl b/applications/PoP/poc/res/shadow/sk26.atl new file mode 100644 index 0000000..e24d76a Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk26.atl differ diff --git a/applications/PoP/poc/res/shadow/sk27.atl b/applications/PoP/poc/res/shadow/sk27.atl new file mode 100644 index 0000000..81600d0 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk27.atl differ diff --git a/applications/PoP/poc/res/shadow/sk3.atl b/applications/PoP/poc/res/shadow/sk3.atl new file mode 100644 index 0000000..e2a92c1 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk3.atl differ diff --git a/applications/PoP/poc/res/shadow/sk4.atl b/applications/PoP/poc/res/shadow/sk4.atl new file mode 100644 index 0000000..358078e Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk4.atl differ diff --git a/applications/PoP/poc/res/shadow/sk5.atl b/applications/PoP/poc/res/shadow/sk5.atl new file mode 100644 index 0000000..8b92d5f Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk5.atl differ diff --git a/applications/PoP/poc/res/shadow/sk6.atl b/applications/PoP/poc/res/shadow/sk6.atl new file mode 100644 index 0000000..39b4993 Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk6.atl differ diff --git a/applications/PoP/poc/res/shadow/sk7.atl b/applications/PoP/poc/res/shadow/sk7.atl new file mode 100644 index 0000000..65d80bb Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk7.atl differ diff --git a/applications/PoP/poc/res/shadow/sk8.atl b/applications/PoP/poc/res/shadow/sk8.atl new file mode 100644 index 0000000..75ac4ce Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk8.atl differ diff --git a/applications/PoP/poc/res/shadow/sk9.atl b/applications/PoP/poc/res/shadow/sk9.atl new file mode 100644 index 0000000..a69cacf Binary files /dev/null and b/applications/PoP/poc/res/shadow/sk9.atl differ diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md index a764b2f..1369e5b 100644 --- a/applications/PoP/roomtest/BUGS_OPEN.md +++ b/applications/PoP/roomtest/BUGS_OPEN.md @@ -24,6 +24,7 @@ | [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику | | [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** | | [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов | +| [BUG-CORPSE-AIR](#bug-corpse-air) | труп Кида висит в воздухе, когда убившая его плита улетела вниз | физика | открыт | | [LOOSE-ROOM-CHANGE](#loose-room-change) | расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату | порт | закрыт 2026-08-13, кроме [LOOSE-SEAM-ANIM](#loose-seam-anim) | | [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** | | [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 | @@ -870,3 +871,25 @@ Makefile правила нет. **Оговорка.** `pop_guard_set_palette` для типа 4 звать НЕЛЬЗЯ — она затрёт палитру Тени палитрой стража (`curr_guard_color` у не-стражей = 0, `seg002:183`). + +### BUG-CORPSE-AIR. Труп висит в воздухе после падения плиты + +**Симптом** (найдено пользователем 2026-08-20, ур. 12 к. 15, старт с +`POS=15`): Кид встал на проваливающийся пол (0,6), плита обрушилась и +убила его, но ТЕЛО осталось лежать на уровне исчезнувшего пола, а плита +улетела дальше вниз. Труп висит в пустоте. + +**Где смотреть.** Кадр смерти — `actions_5_bumped` (см. seq в +`kid_data`), а гравитация в `check_action` (seg006:05CD) для действия 5 +включается только на кадрах приседа: у оригинала есть отдельные фиксы +`FIX_DEAD_FLOATING_IN_AIR` (кадры 177..185) именно про этот случай — +значит **в оригинале 1989 года труп в воздухе тоже висит**, и SDLPoP +чинит это опциональным фиксом, по умолчанию выключенным. + +**Отсюда вопрос к решению**, а не сразу правка: чинить ли вообще. Если +чинить — портировать `fix_dead_floating_in_air` и записать как +осознанное расхождение в `../docs/impl_diff.md` (мы уже отказались от +двух других опциональных фиксов SDLPoP в `bump_into_opponent`). + +**Приоритет низкий:** ситуация возникает только когда труп остаётся на +проваливающемся полу, играбельности не мешает. diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 93af3ee..d81074e 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c $(PROF_FLAGS) +EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте @@ -112,8 +112,16 @@ VIZIER_DIR := $(CURDIR)/../poc/res/vizier VIZIER_DATA := $(VIZIER_DIR)/g0.atl $(VIZIER_DIR)/g1.atl $(VIZIER_DIR)/g2.atl \ $(VIZIER_DIR)/g3.atl $(VIZIER_DIR)/g4.atl +# ТЕНЬ — свой набор, запечённый упаковщиком (наложение спрайта на себя со +# сдвигом; наш блит так не умеет, см. ../docs/shadow_atlas_plan.md). Две +# половины в одном наборе: sk* — кадры вне боя (спрайты Кида), sf* — кадры +# 150..189 (SHADOW.DAT, это тоже графика Кида, а не стража). +SHADOW_DIR := $(CURDIR)/../poc/res/shadow +SHADOW_DATA := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(SHADOW_DIR)/sk$(n).atl) \ + $(foreach n,0 1 2 3,$(SHADOW_DIR)/sf$(n).atl) + EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \ - $(VIZIER_DATA) $(LVL_DATA) + $(VIZIER_DATA) $(SHADOW_DATA) $(LVL_DATA) include $(PROJ_ROOT)/app.mk # Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS), @@ -130,6 +138,7 @@ hdd: $(EXAMPLE).exe $(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(SKEL_DATA),SKEL:$(f)) \ $(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \ + $(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \ $(foreach f,$(LVL_DATA),LEVELS:$(f)) TC := $(PROJ_ROOT)/applications/PoP/toolchain @@ -144,6 +153,9 @@ $(SKEL_DATA): $(TC)/pop_pack_guard.py $(VIZIER_DATA): $(TC)/pop_pack_guard.py cd $(TC) && python3 pop_pack_guard.py VIZIER +$(SHADOW_DATA) pop_shadow_atlas.h: $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py + cd $(TC) && python3 pop_pack_shadow.py + $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py cd $(TC) && python3 pop_pack_kid.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index d24823b..fcdbcb4 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -23,6 +23,7 @@ #include "_pop_kdraw.h" #include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */ #include "pop_cdraw.h" +#include "pop_shadow.h" /* запечённый набор Тени */ #include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_state.h" /* ВРЕМЕННО: зонды pop_dbg_b* */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */ @@ -527,6 +528,10 @@ void pop_char_draw(uint8_t who) __banked const pop_char_t *ch; const kframe *fr; const atlas_t *pages; + /* Атлас БРЫЗГ отдельно от кадра: у оригинала они всегда из «своего» + * chtab (Кид — chtab_2 image 218, прочие — chtab_5 image 1, + * seg006:2019), а у Тени кадр может прийти из другой половины набора. */ + const atlas_t *spl; const uint8_t *img; uint8_t npages, page, idx, dp, flip, hurt, lskip; uint8_t w, h, vis_w; @@ -549,19 +554,31 @@ void pop_char_draw(uint8_t who) __banked if (who == POP_CH_KID) { pop_loadkid(); ch = &Kid; fr = &kid_frame; pages = kidp; npages = kid_npages; + spl = kidp; hurt = pop_kid_hurt; } else { pop_loadshad(); ch = &Guard; fr = &pop_gframe; pages = gp; npages = g_ok ? GUARD_PAGES : 0; + spl = gp; hurt = pop_guard_hurt; if (Guard.charid == 0) return; /* соперника на сцене нет */ - /* ТЕНЬ (ур. 4) — это зеркальный Кид: вне боевых кадров 150..189 она - * ходит по таблице КИДА (seg006:0532), а `image` из неё индексирует - * спрайты Кида, а не стража. Набор атласов обязан идти за таблицей — - * условие берём из того же места, что load_frame, чтобы они не - * разъехались. */ - if (!pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) { + /* Набор атласов обязан идти за ТАБЛИЦЕЙ КАДРОВ — условие берём из + * того же места, что load_frame (seg006:0532), чтобы они не + * разъехались: вне боевых кадров 150..189 Тень ходит по таблице + * КИДА, и `image` из неё индексирует спрайты Кида. */ + if (Guard.charid == CHARID_1_SHADOW && pop_shadow_kid_n) { + /* У Тени СВОЙ запечённый набор (pop_shadow.h): её вид даёт + * наложение спрайта на себя со сдвигом, а пакетный блит так не + * умеет. Обе половины лежат в нём же, поэтому подмены на kidp + * здесь уже не нужно. */ + if (pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) { + pages = pop_shadow_fgt; npages = pop_shadow_fgt_n; + } else { + pages = pop_shadow_kid; npages = pop_shadow_kid_n; + } + spl = pop_shadow_fgt; /* брызги — из половины chtab_5 */ + } else if (!pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) { pages = kidp; npages = kid_npages; } } @@ -741,7 +758,7 @@ void pop_char_draw(uint8_t who) __banked /* по нему считается футпринт */ } pop_dbg_m15(); /* ЗАМЕР: снимок прямоугольника сделан */ - if (hurt) cd_splash(s, who, pages, fp_x, obj_y); + if (hurt) cd_splash(s, who, spl, fp_x, obj_y); /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх * персонажа — со своим прямоугольником heal. */ { diff --git a/applications/PoP/roomtest/pop_shadow.c b/applications/PoP/roomtest/pop_shadow.c new file mode 100644 index 0000000..34e3b3d --- /dev/null +++ b/applications/PoP/roomtest/pop_shadow.c @@ -0,0 +1,88 @@ +/* + * pop_shadow.c — загрузка запечённого набора спрайтов Тени. Зачем набор + * отдельный и почему половин две — в шапке pop_shadow.h. + * + * Код ХОЛОДНЫЙ (раз на уровень), поэтому живёт в банке 8 вместе с прочей + * загрузкой. Сами atlas_t — в W2: их читает отрисовка из банка 4, а + * данные видны из любого банка. + */ + +#include +#include +#include +#include "pop_shadow.h" +#include "pop_shadow_atlas.h" + +atlas_t pop_shadow_kid[POP_SHADOW_KID_PAGES]; +atlas_t pop_shadow_fgt[POP_SHADOW_FGT_PAGES]; +uint8_t pop_shadow_kid_n, pop_shadow_fgt_n; + +/* ГРУЗИМ ОДИН РАЗ НА ЗАПУСК, а не по уровням. + * + * Тень бывает на ЧЕТЫРЁХ уровнях: 4 — рождается прыжком сквозь зеркало + * (jump_through_mirror, seg003:0811, ставит Char.charid = charid_1_shadow + * прямо из Кида), 5 — крадёт зелье, 6 — прыгает через пропасть, 12 — + * дерётся. Читается как три, потому что таблиц init_shad_N всего три + * (5/6/12): четвёртой нет — на 4-м Тень копируется с Кида, а не поднимается + * из таблицы. Так вот, напрашивается + * грузить набор только там, как это сделано с перевёрнутыми наборами + * (pop_vflip_load_all зовётся лишь на POP_UPSIDE_LEVEL). Решение + * пользователя — грузить сразу, и оно правильное по двум причинам: + * страниц EMM с запасом (32 из ~215 свободных), а вот забыть перезагрузку + * на границе уровня легко — ровно так и появился BUG-SHADOW-SET. Разовая + * загрузка убирает целый класс ошибок и заодно не тормозит переход между + * уровнями на 32 файла. */ +int pop_shadow_load(void) __banked +{ + uint8_t i; + if (pop_shadow_kid_n || pop_shadow_fgt_n) return 0; /* уже загружен */ + + for (i = 0; i < POP_SHADOW_KID_PAGES; i++) { + if (atlas_load(&pop_shadow_kid[i], pop_shadow_kid_atl[i]) != 0) { + pop_shadow_free(); + return -1; + } + pop_shadow_kid_n = (uint8_t)(i + 1); + } + for (i = 0; i < POP_SHADOW_FGT_PAGES; i++) { + if (atlas_load(&pop_shadow_fgt[i], pop_shadow_fgt_atl[i]) != 0) { + pop_shadow_free(); + return -1; + } + pop_shadow_fgt_n = (uint8_t)(i + 1); + } + + pop_shadow_pal_apply(); + return 0; +} + +/* ПАЛИТРА ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика. + * + * `gfx_pal_fload("KID\\kid.pal")` заливает ВСЕ 256 записей — это единая + * игровая палитра, — и затирает любые слоты, выставленные до него. Ровно + * та же беда уже была с тайлсетом: сразу за ним стоит pop_bg_pal_apply, + * который возвращает цвета env/wall. Наши 0xA0..0xAF из той же оперы, и + * в первом заходе я на это наступил: набор грузился, силуэт рисовался + * правильной формы, но ЦЕЛИКОМ ЧЁРНЫЙ (поймано пользователем на 6-м + * уровне). Поэтому вызов живёт рядом с pop_bg_pal_apply, а не внутри + * загрузки атласов. + * + * Обе страницы дабл-буфера: у них свои палитры. Через буфер в W2 — + * gfx_pal_load отдаёт указатель BIOS ($A4), а тот читает только + * #4000-#BFFF и банковую rodata не увидит. */ +void pop_shadow_pal_apply(void) __banked +{ + uint8_t buf[64], k; + if (!pop_shadow_kid_n) return; /* набора нет — нечего красить */ + for (k = 0; k < 64; k++) buf[k] = pop_shadow_pal[k]; + gfx_pal_load(0, POP_SHADOW_PAL_BASE, 16, buf); + gfx_pal_load(1, POP_SHADOW_PAL_BASE, 16, buf); +} + +void pop_shadow_free(void) __banked +{ + uint8_t i; + for (i = 0; i < pop_shadow_kid_n; i++) atlas_free(&pop_shadow_kid[i]); + for (i = 0; i < pop_shadow_fgt_n; i++) atlas_free(&pop_shadow_fgt[i]); + pop_shadow_kid_n = pop_shadow_fgt_n = 0; +} diff --git a/applications/PoP/roomtest/pop_shadow.h b/applications/PoP/roomtest/pop_shadow.h new file mode 100644 index 0000000..7463e2f --- /dev/null +++ b/applications/PoP/roomtest/pop_shadow.h @@ -0,0 +1,55 @@ +/* + * pop_shadow.h — запечённый набор спрайтов ТЕНИ. + * + * Оригинал спрайтов Тени не хранит: он кладёт обычный спрайт дважды — в x + * прозрачным блитом и в x+1 «блиттером XOR» (draw_objtable_item, + * seg008.c:1600). Пакетный блит так не умеет, поэтому результат запечён + * заранее (toolchain/pop_pack_shadow.py); разбор и замеры — + * ../docs/shadow_atlas_plan.md. + * + * ПОЛОВИН ДВЕ, и это не наша выдумка. Набор спрайтов у оригинала выбирает + * не персонаж, а поле `sword` САМОГО КАДРА (load_frame_to_obj, + * seg008.c:1752: chtab_base жёстко = chtab_2, плюс `sword >> 6`). У всех + * кадров таблицы Кида эти биты нулевые, у всех кадров таблицы стража — + * 0xC0. Тень берёт таблицу стража для кадров 150..189 (seg006.c:533), + * значит идёт через chtab_5 — а chtab_5 на 12-м уровне грузится из + * SHADOW.DAT (tbl_guard_type[12] == 4), и это графика КИДА в боевых позах. + * Отсюда: + * + * pop_shadow_kid[] — кадры вне боя (запечены из спрайтов Кида); + * pop_shadow_fgt[] — кадры 150..189 (запечены из SHADOW.DAT). + * + * Какая половина нужна, отрисовка уже знает — тем же условием, что + * выбирает таблицу кадров (pop_frame_tbl_is_guard). + * + * СПРАЙТ ТЕНИ НА ПИКСЕЛЬ ШИРЕ исходного: второй проход оригинала кладётся + * в x+1, суммарный след занимает [x, x+w]. Рисовать по тем же + * координатам, что и Кида — сдвиг уже внутри спрайта. + */ +#ifndef POP_SHADOW_H +#define POP_SHADOW_H + +#include +#include + +extern atlas_t pop_shadow_kid[]; /* половина «вне боя» */ +extern atlas_t pop_shadow_fgt[]; /* половина «в бою» (SHADOW.DAT) */ +/* Сколько страниц реально загружено; 0 = набора нет (уровень без Тени + * или загрузка не удалась) — отрисовка тогда идёт прежним путём. */ +extern uint8_t pop_shadow_kid_n, pop_shadow_fgt_n; + +/* Загрузить набор. Звать ОДИН РАЗ при старте: набор не зависит от + * уровня, а перезагрузка на границе — лишний повод её забыть (почему + * именно так — в шапке pop_shadow.c). Повторный вызов — no-op. + * Возврат: 0 — OK, -1 — ошибка загрузки. */ +int pop_shadow_load(void) __banked; + +/* Залить 16 слотов палитры Тени. ЗВАТЬ ПОСЛЕ ЛЮБОЙ заливки общей + * палитры (gfx_pal_fload kid.pal): та кладёт все 256 записей и затирает + * наши — см. развёрнуто в pop_shadow.c. Рядом с pop_bg_pal_apply. */ +void pop_shadow_pal_apply(void) __banked; + +/* Освободить страницы; звать на выходе из программы. */ +void pop_shadow_free(void) __banked; + +#endif diff --git a/applications/PoP/roomtest/pop_shadow_atlas.h b/applications/PoP/roomtest/pop_shadow_atlas.h new file mode 100644 index 0000000..7c46fe7 --- /dev/null +++ b/applications/PoP/roomtest/pop_shadow_atlas.h @@ -0,0 +1,71 @@ +/* pop_shadow_atlas.h — раскладка атласа ТЕНИ. + * СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную. + * + * Две половины, потому что и у оригинала их две (см. шапку + * упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида), + * кадры 150..189 — chtab_5, а он на 12-м уровне это SHADOW.DAT. + * Какая половина нужна, приложение уже знает — тем же условием, + * что выбирает таблицу кадров (pop_frame_tbl_is_guard). + * + * Спрайт Тени НА ОДИН ПИКСЕЛЬ ШИРЕ исходного: второй проход + * оригинала кладётся в x+1, и суммарный след занимает [x, x+w]. + * Рисовать по тем же координатам, что и Кида — сдвиг уже внутри. + */ +#ifndef POP_SHADOW_ATLAS_H +#define POP_SHADOW_ATLAS_H + +#include + +#define POP_SHADOW_SHIFT 3 +#define POP_SHADOW_MASK 7 +#define POP_SHADOW_PAL_BASE 0xA0 /* слоты Sprinter под Тень */ + +/* Половина «вне боя» — кадры Кида (chtab_2). */ +#define POP_SHADOW_KID_PAGES 28 +/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */ +#define POP_SHADOW_FGT_PAGES 4 + +static const char *const pop_shadow_kid_atl[POP_SHADOW_KID_PAGES] = { + "SHADOW\\sk0.atl", + "SHADOW\\sk1.atl", + "SHADOW\\sk2.atl", + "SHADOW\\sk3.atl", + "SHADOW\\sk4.atl", + "SHADOW\\sk5.atl", + "SHADOW\\sk6.atl", + "SHADOW\\sk7.atl", + "SHADOW\\sk8.atl", + "SHADOW\\sk9.atl", + "SHADOW\\sk10.atl", + "SHADOW\\sk11.atl", + "SHADOW\\sk12.atl", + "SHADOW\\sk13.atl", + "SHADOW\\sk14.atl", + "SHADOW\\sk15.atl", + "SHADOW\\sk16.atl", + "SHADOW\\sk17.atl", + "SHADOW\\sk18.atl", + "SHADOW\\sk19.atl", + "SHADOW\\sk20.atl", + "SHADOW\\sk21.atl", + "SHADOW\\sk22.atl", + "SHADOW\\sk23.atl", + "SHADOW\\sk24.atl", + "SHADOW\\sk25.atl", + "SHADOW\\sk26.atl", + "SHADOW\\sk27.atl", +}; + +static const char *const pop_shadow_fgt_atl[POP_SHADOW_FGT_PAGES] = { + "SHADOW\\sf0.atl", + "SHADOW\\sf1.atl", + "SHADOW\\sf2.atl", + "SHADOW\\sf3.atl", +}; + +/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */ +static const uint8_t pop_shadow_pal[64] = { + 0x00,0x00,0x00,0x00,0xD8,0xFC,0xFC,0x00,0x6C,0x80,0xDC,0x00,0x78,0x44,0x24,0x00,0xA0,0xB8,0xD8,0x00,0xB5,0x7C,0x21,0x00,0x00,0x90,0xB8,0x00,0x5D,0x6D,0xAB,0x00,0x0C,0xF0,0x6C,0x00,0xCC,0x38,0x05,0x00,0x38,0xCC,0xC0,0x00,0x58,0xDB,0xA7,0x00,0xBB,0x9F,0x54,0x00,0x5D,0x18,0x60,0x00,0x37,0xD7,0x37,0x00,0x2F,0x17,0xD2,0x00, +}; + +#endif diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index d3f7c08..774fe5e 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -23,6 +23,7 @@ #include /* gfx_copy_page */ #include "pop_tune.h" #include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */ +#include "pop_shadow.h" /* запечённый набор Тени */ #include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */ #include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */ #include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */ @@ -749,6 +750,7 @@ int pop_boot(void) __banked } pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */ pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ + pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ puts("kid atlas load failed"); return -1; @@ -771,6 +773,8 @@ int pop_boot(void) __banked * дворцовых уровнях без этого рисуется дворцовая геометрия подземельными * красками (сине-серое вместо песочного). */ pop_bg_pal_apply(); + /* ...и палитру Тени: kid.pal затёр и её 16 слотов (pop_shadow.c). */ + pop_shadow_pal_apply(); /* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ @@ -853,6 +857,7 @@ void pop_shutdown(void) __banked closegraph(); pop_bg_free(); pop_kid_free(); + pop_shadow_free(); pop_guard_free(); pop_kid_data_free(); pop_level_free(); diff --git a/applications/PoP/toolchain/pop_pack_shadow.py b/applications/PoP/toolchain/pop_pack_shadow.py new file mode 100644 index 0000000..aac74fb --- /dev/null +++ b/applications/PoP/toolchain/pop_pack_shadow.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +""" +pop_pack_shadow.py — упаковщик спрайтов ТЕНИ. + +ЗАЧЕМ ОТДЕЛЬНЫЙ НАБОР. Оригинал не хранит спрайтов Тени вовсе: он кладёт +обычный спрайт ДВАЖДЫ — в x обычным прозрачным блитом, и в x+1 «блиттером +XOR» (draw_objtable_item, seg008.c:1600). Наш пакетный блит так не умеет, +поэтому результат запекается заранее, здесь. + +ЧТО ИМЕННО ДЕЛАЕТ ОРИГИНАЛ (проверено по исходнику, не по памяти): + + * `blitters_2_or` — НЕ побитовое ИЛИ. В SDLPoP это обычный блит с + colour key = индекс 0 (method_6_blit_img_to_scr, seg009.c:3306), то + есть один в один наш прозрачный блит. + * `blitters_3_xor` работает по 24-битному RGB, а не по индексам палитры + (blit_xor, seg009.c:3190: конвертация в 24 бита, затем `*p_dest ^= + *p_src` побайтно). Прозрачности у него нет — XOR'ится весь + прямоугольник, но прозрачный пиксель это чёрный 0x000000, а XOR с + нулём ничего не меняет. + +Отсюда своя палитра: XOR двух цветов игровой палитры даёт цвет, которого в +ней нет. + +ЕДИНСТВЕННОЕ РАСХОЖДЕНИЕ. XOR считается по тому, что УЖЕ на экране, то +есть зависит от фона. Разбор по пикселям: от фона зависит только кайма в +один пиксель по ЛЕВЫМ кромкам силуэта (там, где спрайт прозрачен в x, но +непрозрачен в x−1). На чёрном фоне `фон XOR цвет == цвет`, и запечка +точна; Тень почти всегда на чёрном. Мы печём именно этот случай. + +ДВА НАБОРА В ОДНОМ АТЛАСЕ. Набор спрайтов у оригинала выбирает НЕ +персонаж, а поле `sword` самого кадра: load_frame_to_obj (seg008.c:1752) +держит chtab_base жёстко равным chtab_2 и прибавляет `sword >> 6`. У всех +кадров frame_table_kid эти биты нулевые, у всех кадров frame_tbl_guard — +0xC0. Тень берёт таблицу стража для кадров 150..189 (seg006.c:533), то +есть идёт через chtab_5, а chtab_5 на 12-м уровне грузится из SHADOW.DAT +(tbl_guard_type[12] == 4, seg000.c:1117) — и это графика КИДА в боевых +позах, а не стража. Поэтому печём обе половины: + + KID — data/KID/res(401+id).png, id 0..218 -> sk0..skN.atl + SHADOW — data/SHADOW/res(751+id).png, id 0..31 -> sf0..sfM.atl + +Выход: poc/res/shadow/sk*.atl + sf*.atl + pop_shadow_atlas.h (палитра +внутри заголовка — 16 записей, её заливает приложение через gfx_pal_load). +""" +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 +from pop_pack_kid import transpose_cols + +DATA = os.path.join(HERE, "..", "SDLPoP", "data") +OUT_DIR = os.path.join(HERE, "..", "poc", "res", "shadow") +PAL_BASE = 0xA0 # слоты Sprinter под Тень: 0xA0..0xAF +NCOL = 16 # столько цветов в палитре Тени +SHIFT = 3 # sk[id>>3], idx id&7 — как у Kid (кадры крупные) +MASK = (1 << SHIFT) - 1 + +# Половины набора: (каталог, база ресурса, префикс выходных файлов). +# Off-by-one тот же, что у Kid и стража: images[0] = res(база+1), значит +# frame.image = N -> res(база+1+N) (см. pop_pack_kid.load_kid). +HALVES = (("KID", 400, "sk"), + ("SHADOW", 750, "sf")) + + +def frames(dirname, base): + """Все кадры половины: [(id, w, h, [[rgb|None]...])] — уже после + наложения XOR, в честном RGB.""" + out = [] + id = 0 + while True: + path = os.path.join(DATA, dirname, f"res{base + 1 + id}.png") + if not os.path.exists(path): + break + im = Image.open(path) + if im.mode != "P": + B.die(f"{path}: mode {im.mode}, ожидался P") + pal = im.getpalette()[:NCOL * 3] + rgb = [tuple(pal[i * 3:i * 3 + 3]) for i in range(NCOL)] + w, h = im.size + px = im.load() + grid = [] + for y in range(h): + # Ширина на один больше: второй проход кладётся в x+1, и + # суммарный след занимает [x, x+w]. + row = [None] * (w + 1) + for x in range(w): # проход 1: обычный блит + i = px[x, y] + if i: + row[x] = rgb[i] + for x in range(w): # проход 2: XOR в x+1 + i = px[x, y] + if not i: + continue # чёрный: XOR — не операция + s = rgb[i] + t = x + 1 + row[t] = s if row[t] is None else tuple(a ^ b for a, b in zip(row[t], s)) + grid.append(row) + out.append((id, w + 1, h, grid)) + id += 1 + return out + + +def histogram(halves): + h = {} + for _, fr in halves: + for _, _, _, grid in fr: + for row in grid: + for c in row: + if c is not None: + h[c] = h.get(c, 0) + 1 + return h + + +def _d2(a, b): + return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2 + + +def _kmeans(pts, k, init): + """Взвешенный Ллойд по СПИСКУ ЦВЕТОВ с весами (их полсотни, не пиксели).""" + cent = list(init) + for _ in range(80): + groups = [[] for _ in cent] + for c, n in pts: + groups[min(range(len(cent)), key=lambda j: _d2(c, cent[j]))].append((c, n)) + new = [] + for j, g in enumerate(groups): + if not g: + new.append(cent[j]) + continue + w = sum(n for _, n in g) + new.append(tuple(int(round(sum(c[i] * n for c, n in g) / w)) for i in range(3))) + if new == cent: + break + cent = new + return cent + + +def build_palette(hist): + """16 цветов ГИБРИДОМ: часть слотов — самые частые цвета ТОЧНО, остаток + — взвешенные кластеры хвоста. + + Почему не «просто самые частые»: тогда редкие цвета уезжают в далёкий + ближайший, и грубых промахов (сдвиг >90 по евклиду в RGB при максимуме + 441) получается впятеро больше — 381 пиксель против 76 на все 251 + кадр (замер 2026-08-20). Почему не чистый k-means: он двигает вообще + все цвета, включая массовые. + + Разделение слотов подбираем перебором по критерию «сначала минимум + грубых промахов, потом заметных, потом худшая ошибка».""" + items = sorted(hist.items(), key=lambda kv: -kv[1]) + + def stats(pal): + bad = worst = mid = big = 0 + for c, n in items: + e = min(_d2(c, p) for p in pal) ** 0.5 + if e > 0: + bad += n + worst = max(worst, e) + if e >= 90: + big += n + elif e >= 40: + mid += n + return bad, mid, big, worst + + best = None + for m in range(1, NCOL): # m слотов под хвост + keep = [c for c, _ in items[:NCOL - m]] + rest = [(c, n) for c, n in items[NCOL - m:]] + if not rest: + continue + pal = keep + _kmeans(rest, m, [c for c, _ in rest[:m]]) + bad, mid, big, worst = stats(pal) + score = (big, mid, worst) + if best is None or score < best[0]: + best = (score, m, pal, (bad, mid, big, worst)) + _, m, pal, st = best + # Чёрный обязан быть ТОЧНЫМ и лежать в слоте 0: это XOR-погашенная + # середина силуэта, больше половины всех пикселей Тени. + j = min(range(len(pal)), key=lambda i: _d2((0, 0, 0), pal[i])) + pal[j] = (0, 0, 0) + pal[0], pal[j] = pal[j], pal[0] + bad, mid, big, worst = st + tot = sum(hist.values()) + print(f" палитра: {NCOL - m} частых точно + {m} кластеров хвоста; " + f"из {len(items)} цветов изменено {bad} px из {tot} " + f"({100.0 * bad / tot:.2f} %), заметно {mid}, сильно {big}, худшая {worst:.0f}") + return pal + + +def encode(grid, w, h, pal): + """RGB-сетка -> байты атласа: нет пикселя -> 0xFF, цвет -> PAL_BASE + k. + Чёрный — НАСТОЯЩИЙ цвет Тени (слот 0), а не прозрачность.""" + row = bytearray() + for y in range(h): + for x in range(w): + c = grid[y][x] + if c is None: + row.append(0xFF) + else: + row.append(PAL_BASE + min(range(len(pal)), key=lambda k: _d2(c, pal[k]))) + return transpose_cols(w, h, bytes(row)) + + +def write_header(path, counts, pal): + vals = [] + for (r, g, b) in pal: + vals += [b, g, r, 0] # формат gfx_pal_load: B, G, R, 0 + lines = [ + "/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.", + " * СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную.", + " *", + " * Две половины, потому что и у оригинала их две (см. шапку", + " * упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),", + " * кадры 150..189 — chtab_5, а он на 12-м уровне это SHADOW.DAT.", + " * Какая половина нужна, приложение уже знает — тем же условием,", + " * что выбирает таблицу кадров (pop_frame_tbl_is_guard).", + " *", + " * Спрайт Тени НА ОДИН ПИКСЕЛЬ ШИРЕ исходного: второй проход", + " * оригинала кладётся в x+1, и суммарный след занимает [x, x+w].", + " * Рисовать по тем же координатам, что и Кида — сдвиг уже внутри.", + " */", + "#ifndef POP_SHADOW_ATLAS_H", + "#define POP_SHADOW_ATLAS_H", + "", + "#include ", + "", + "#define POP_SHADOW_SHIFT %d" % SHIFT, + "#define POP_SHADOW_MASK %d" % MASK, + "#define POP_SHADOW_PAL_BASE 0x%02X /* слоты Sprinter под Тень */" % PAL_BASE, + "", + "/* Половина «вне боя» — кадры Кида (chtab_2). */", + "#define POP_SHADOW_KID_PAGES %d" % counts["sk"], + "/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */", + "#define POP_SHADOW_FGT_PAGES %d" % counts["sf"], + "", + "static const char *const pop_shadow_kid_atl[POP_SHADOW_KID_PAGES] = {", + ] + lines += [' "SHADOW\\\\sk%d.atl",' % i for i in range(counts["sk"])] + lines += [ + "};", + "", + "static const char *const pop_shadow_fgt_atl[POP_SHADOW_FGT_PAGES] = {", + ] + lines += [' "SHADOW\\\\sf%d.atl",' % i for i in range(counts["sf"])] + lines += [ + "};", + "", + "/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */", + "static const uint8_t pop_shadow_pal[64] = {", + " " + ",".join("0x%02X" % v for v in vals) + ",", + "};", + "", + "#endif", + "", + ] + with open(path, "w") as f: + f.write("\n".join(lines)) + print(f" pop_shadow_atlas.h -> {path}") + + +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + halves = [(prefix, frames(d, base)) for d, base, prefix in HALVES] + for (d, base, prefix), (_, fr) in zip(HALVES, halves): + if not fr: + B.die(f"нет спрайтов в data/{d} (SHADOW.DAT распакован?)") + print(f" {d}: {len(fr)} кадров") + pal = build_palette(histogram(halves)) + + counts, total, nspr = {}, 0, 0 + for prefix, fr in halves: + pages = {} + for id, w, h, grid in fr: + pages.setdefault(id >> SHIFT, {})[id & MASK] = (w, h, encode(grid, w, h, pal)) + nspr += 1 + npg = max(pages) + 1 + counts[prefix] = npg + for pg in range(npg): + nm = f"{prefix}{pg}.atl" + cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {})) + total += sz + print(f" {prefix}*.atl: {len(fr)} спрайтов, {npg} EMM-страниц") + write_header(os.path.join(HERE, "..", "roomtest", "pop_shadow_atlas.h"), counts, pal) + print(f"ИТОГО: {nspr} спрайтов Тени, " + f"{counts['sk'] + counts['sf']} EMM-страниц, {total} Б -> {OUT_DIR}") + + +if __name__ == "__main__": + main()