diff --git a/applications/PoP/docs/shadow_atlas_plan.md b/applications/PoP/docs/shadow_atlas_plan.md index c20ba52..c4a12a5 100644 --- a/applications/PoP/docs/shadow_atlas_plan.md +++ b/applications/PoP/docs/shadow_atlas_plan.md @@ -33,6 +33,51 @@ case 1: // shadow Отсюда и берётся необходимость СВОЕЙ палитры: XOR двух цветов игровой палитры даёт цвет, которого в ней нет. +### Каким набором спрайтов рисуется Тень (проверено 2026-08-20) + +Сначала я решил, что в боевых кадрах Тень рисуется спрайтами СТРАЖА, и +записал это в план. **Это было неверно, поправка ниже.** + +Набор выбирает НЕ charid. `load_frame_to_obj` (`seg008.c:1752`): + +```c +word chtab_base = id_chtab_2_kid; // жёстко Кид +obj_chtab = chtab_base + (cur_frame.sword >> 6); // старшие 2 бита кадра +``` + +то есть набор берётся из САМИХ ДАННЫХ КАДРА, поле `sword`: младшие 6 бит — +картинка меча, старшие два — смещение chtab относительно Кида +(`types.h:361`). Проверил обе таблицы: + +- `frame_table_kid` — **все 241 кадра** имеют `sword & 0xC0 == 0` → chtab_2. + Значит **Кид всегда рисуется своими спрайтами**, боевые кадры 150..189 не + исключение; +- `frame_tbl_guard` — все кадры имеют `0xC0` → chtab_5. + +Тень берёт `frame_tbl_guard` для кадров 150..189 (`seg006.c:533`), значит в +бою она идёт через chtab_5. **Но chtab_5 — это не «страж», это «соперник +уровня»**: он грузится из `tbl_guard_dat[tbl_guard_type[уровень]]` +(`seg000.c:1117`), а `tbl_guard_type[12] == 4` → **SHADOW.DAT**. + +Открыл SHADOW.DAT: его палитра **побайтно равна палитре Кида** (у GUARD.DAT +там серая рампа под перекраску), а спрайты — Кид в боевых позах, не страж. +Отрендерил тройками «Кид / SHADOW.DAT / GUARD.DAT» для кадров +151/153/158/161/167: первые две колонки — один и тот же персонаж, третья — +серый страж в тюрбане. + +**Вывод: Тень ВСЕГДА выглядит Кидом, и ощущение пользователя верно.** +Спрайты при этом лежат в двух файлах: не-боевые кадры в chtab_2 (KID), +боевые — в chtab_5, заполненном SHADOW.DAT. + +### Можно ли взять для боя собственные кадры Кида + +Механически да: `frame_table_kid` покрывает 150..189 со своей геометрией, +и хватило бы снять спецветку для `charid_1_shadow` в `load_frame`. Но +кадры НЕ совпадают: из 34 боевых у 31 отличается габарит (на 1-6 px), у 3 +отличаются пиксели. Это разная графика, а не одна и та же в двух файлах. +Поэтому берём SHADOW.DAT — он и есть «кадры Кида для Тени», подготовленные +авторами. + ## 2. Единственное место, где запечка отличается от оригинала XOR идёт по тому, что УЖЕ на экране, то есть результат зависит от фона. @@ -53,21 +98,18 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре ## 3. Замеры на реальных ассетах -Прогон алгоритма по всем кадрам (`SDLPoP/data/KID`, `SDLPoP/data/GUARD`): +Прогон алгоритма по правильным наборам (`SDLPoP/data/KID` + +`SDLPoP/data/SHADOW`): | набор | кадров | макс. габарит | разных цветов | |---|---:|---|---:| | Кид (chtab_2) | 219 | 56×57 | 44 | -| Страж (chtab_5) | 34 | 53×42 | 16 | -| **вместе** | **253** | 57×57 (с учётом +1 px сдвига) | **59** | +| SHADOW.DAT (chtab_5 на ур. 12) | 32 | 49×38 | 22 | +| **вместе** | **251** | 57×57 (с учётом +1 px сдвига) | **45** | -Всего пикселей во всех кадрах Тени — 96 746. Квантование до N цветов -(оставляем N самых частых, остальные — в ближайший): - -| палитра | затронуто пикселей | доля | худшая ошибка (евклид RGB) | -|---|---:|---:|---:| -| 16 цветов | 3 483 | 3,60 % | 151 | -| **32 цвета** | **426** | **0,44 %** | 113 | +Цветов вместе почти столько же, сколько у одного Кида: SHADOW.DAT сидит на +той же палитре, новых сочетаний XOR почти не даёт. Всего пикселей во всех +кадрах Тени — 91 045. ### Насколько заметна подмена @@ -79,37 +121,34 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре - **B** — взвешенный k-means по всем цветам (двигает вообще все); - **C** — гибрид: часть слотов под точные частые, остаток — кластеры хвоста. -| палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая | средняя | -|---|---|---:|---:|---:|---:|---:| -| 16 | A самые частые | 3 483 (3,60 %) | 2 342 | 257 | 151 | 57,7 | -| 16 | B k-means | 29 368 (30,4 %) | 1 972 | 256 | 142 | 10,9 | -| 16 | C гибрид 10+6 | 7 255 (7,50 %) | 2 146 | **129** | 142 | 33,7 | -| **32** | **A самые частые** | **426 (0,44 %)** | **205** | **27** | 113 | 46,0 | -| 32 | B k-means | 3 425 (3,54 %) | 133 | 27 | 110 | 9,1 | -| 32 | C гибрид 20+12 | 1 997 (2,06 %) | 133 | 27 | 110 | 16,0 | +| палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая | +|---|---|---:|---:|---:|---:| +| 16 | A самые частые | 959 (1,05 %) | 569 | 381 | 128 | +| **16** | **C гибрид 5+11** | 6 984 (7,67 %) | 1 078 | **76** | 114 | +| 32 | A самые частые | 50 (0,05 %) | 40 | 4 | 113 | +| 32 | C гибрид 28+4 | 73 (0,08 %) | 58 | **0** | 90 | -Читается так. При 32 цветах стратегия не важна: «сильно» — ровно **27 -пикселей на все 253 кадра** в любой из трёх, это неустранимые одиночки без -близкого соседа (по 1-3 пикселя на кадр, увидеть нельзя). Перцептивно -значимого остаётся 232 пикселя из 96 746, то есть примерно один пиксель на -кадр. При 16 цветах разница уже есть: гибрид вдвое срезает «сильно» (257 -→ 129), но заметных всё равно две с лишним тысячи. +Читается так. При 32 цветах всё практически идеально: 73 пикселя на 251 +кадр, грубых промахов нет вовсе. При 16 цветах выбор стратегии виден: +«самые частые» трогает меньше пикселей (959), но 381 из них уезжает СИЛЬНО; +гибрид размазывает ошибку — грубых остаётся 76, то есть примерно **0,3 +пикселя на кадр**. -### Решение: **16 цветов, стратегия C (гибрид 10 + 6)** +### Решение: **16 цветов, стратегия C (гибрид 5 + 11)** Числа выше — про пиксели, а решает глаз. Отрендерил одни и те же кадры в трёх видах (точный цвет / 32 / 16) и сравнил в увеличении ×3: **отличий не видно**. Объяснение в самих числах: перцептивно значимых пикселей при -16 цветах 2 275 на 253 кадра, это ~9 пикселей на кадр при ~1 000 видимых, -и они РАССЫПАНЫ по контуру, а не собраны в пятно. +16 цветах гибридом — 1 154 на 251 кадр, это ~4,6 пикселя на кадр при +~1 000 видимых, и они РАССЫПАНЫ по контуру, а не собраны в пятно. Поэтому берём 16, а не 32: экономим блок палитры (пригодится под будущие наборы — принцесса, визирь, мышь), а разница неразличима. **Но стратегия обязана быть гибридной.** При 16 цветах «взять самые -частые» вдвое хуже по грубым промахам (257 пикселей против 129), и это -единственное место, где выбор стратегии виден. Гибрид: 10 самых частых -берём ТОЧНО, оставшиеся 6 слотов отдаём под взвешенные кластеры хвоста. +частые» впятеро хуже по грубым промахам (381 пиксель против 76), и это +единственное место, где выбор стратегии виден. Гибрид: 5 самых частых +берём ТОЧНО, оставшиеся 11 слотов отдаём под взвешенные кластеры хвоста. Если в реальной игре кайма всё же будет резать глаз — переход на 32 цвета это одна константа в упаковщике и один блок палитры, данные не меняются. @@ -142,8 +181,8 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре ## 5. Объём -253 кадра против 219 у Кида — по страницам EMM примерно как нынешний -набор Кида (28 страниц), плюс пара на кадры стража. При 215 свободных +251 кадр против 219 у Кида — по страницам EMM примерно как нынешний +набор Кида (28 страниц), плюс пара на кадры из SHADOW.DAT. При 215 свободных страницах на старте (memory `sprinter_emm_budget`) это не проблема. Кадры смерти («убитый Кид») пока НЕ вырезаем — экономия несколько @@ -152,6 +191,9 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре ## 6. План работ +**Ш0. Довезти SHADOW.DAT.** Его у нас нет вовсе (см. BUG-SHADOW-SET) — +добавить каталог ассетов и правило в Makefile рядом с GUARD/SKEL/VIZIER. + **Ш1. Упаковщик** `toolchain/pop_pack_shadow.py`: прогнать оба набора через алгоритм §1, собрать 16-цветную палитру гибридом (10 точных + 6 кластеров хвоста), выдать @@ -165,9 +207,9 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре **Ш3. Отрисовка**: одно место — `pop_cdraw.c:551..568`, где выбирается `pages`. Сейчас там для соперника берётся `gp`, а для не-боевых кадров -Тени подменяется на `kidp`; добавится ветка «charid == CHARID_1_SHADOW → -shadowp», причём обе половины набора (кадры Кида и кадры стража) лежат в -ОДНОМ атласе Тени, так что подмена становится проще нынешней. +Тени подменяется на `kidp`; станет «charid == CHARID_1_SHADOW → shadowp» +БЕЗ подмены: обе половины (кадры Кида и кадры SHADOW.DAT) лежат в ОДНОМ +атласе Тени, так что ветка становится проще нынешней. **Ш4. Проверка в MAME**: уровень 4 (Тень у зеркала), уровень 6 (Тень крадёт зелье), уровень 12 (бой с Тенью — там она в боевых кадрах, то @@ -179,6 +221,7 @@ shadowp», причём обе половины набора (кадры Кид как `frame + add_frame - 149` (`seg006.c:535`), то есть у нашего атласа Тени нумерация двух половин должна совпадать с тем, что уже делает `pop_frame_tbl_is_guard`. -2. Цвет стража для Тени: у не-стражей `curr_guard_color = 0` - (`seg002:183`), значит запекать половину «кадры стража» надо с - палитрой стража цвета 0, а не с текущей. +2. Перекраска Тени НЕ нужна: SHADOW.DAT приходит уже в палитре Кида, а + `curr_guard_color` у не-стражей равен 0 (`seg002:183`). Ветку + `pop_guard_set_palette` для типа 4 звать нельзя — она затрёт палитру + Тени палитрой стража. diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md index e048d3b..a764b2f 100644 --- a/applications/PoP/roomtest/BUGS_OPEN.md +++ b/applications/PoP/roomtest/BUGS_OPEN.md @@ -23,6 +23,7 @@ | [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз | | [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 у нас нет вовсе | порт | открыт, разбор готов | | [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 | @@ -836,3 +837,36 @@ if (modif) `draw_leveldoor`, либо запечка. Проверять на уровне 1 (подземелье, ширина 39, сдвиг 2) И на уровне 4 (дворец, 48, сдвиг 0) — иначе поймаем только половину. + +### BUG-SHADOW-SET. Тень в бою рисуется спрайтами стража + +**Симптом** (найдено пользователем 2026-08-20): в бою Тень выглядит не +Кидом. В SDLPoP она Кид всегда. + +**Корень.** Набор спрайтов выбирает не charid, а поле `sword` САМОГО +КАДРА: `load_frame_to_obj` (`seg008.c:1752`) держит `chtab_base` жёстко +равным `id_chtab_2_kid` и прибавляет `cur_frame.sword >> 6`. У всех 241 +кадра `frame_table_kid` эти биты нулевые, у всех кадров `frame_tbl_guard` +— `0xC0`. Тень берёт таблицу стража для кадров 150..189 (`seg006.c:533`), +значит идёт через chtab_5. + +Дальше и кроется наша ошибка: **chtab_5 — это не «страж», а «соперник +уровня»**. Он грузится из `tbl_guard_dat[tbl_guard_type[уровень]]` +(`seg000.c:1117`), а `tbl_guard_type[12] == 4` → **SHADOW.DAT**, и это +графика КИДА в боевых позах (палитра SHADOW.DAT побайтно равна палитре +Кида; у GUARD.DAT там серая рампа под перекраску). + +**У нас** `pop_guard_load` (`pop_cdraw.c:98`) разбирает только типы 2 +(скелет) и 3 (визирь), а всё остальное — включая тип 4 — уводит в +`guard_names`. SHADOW.DAT в ассетах отсутствует, каталога нет, в +Makefile правила нет. + +**Починка** — часть работы по атласу Тени, см. +[`../docs/shadow_atlas_plan.md`](../docs/shadow_atlas_plan.md) Ш0/Ш1: +довезти SHADOW.DAT и запечь его во вторую половину атласа Тени. Отдельно +чинить смысла нет: как только Тень поедет своим атласом, ветка выбора +набора для типа 4 исчезнет вместе с проблемой. + +**Оговорка.** `pop_guard_set_palette` для типа 4 звать НЕЛЬЗЯ — она +затрёт палитру Тени палитрой стража (`curr_guard_color` у не-стражей = 0, +`seg002:183`).