Тень: набор спрайтов выбирает поле кадра, а не charid; SHADOW.DAT у нас нет

Поправка к вчерашнему выводу «в бою Тень рисуется спрайтами стража».
Набор берётся из cur_frame.sword>>6 (seg008.c:1752), chtab_base жёстко
равен Киду.  Тень идёт через chtab_5, но chtab_5 — это «соперник уровня»,
и на 12-м это SHADOW.DAT: графика КИДА в боевых позах, палитра побайтно
равна палитре Кида.  Пользователь прав — Тень всегда выглядит Кидом.

Наш pop_guard_load уводит тип 4 в guard_names, SHADOW.DAT в ассетах нет
вообще — заведён BUG-SHADOW-SET.

Пересчитал палитру на правильных наборах: 251 кадр, 45 цветов; 16 цветов
гибридом дают 76 грубых промахов на все кадры (было 129 на ошибочном
наборе).
This commit is contained in:
2026-08-20 10:50:36 +03:00
parent b6660d7694
commit e60a04e900
2 changed files with 115 additions and 38 deletions
+81 -38
View File
@@ -33,6 +33,51 @@ case 1: // shadow
Отсюда и берётся необходимость СВОЕЙ палитры: XOR двух цветов игровой Отсюда и берётся необходимость СВОЕЙ палитры: 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. Единственное место, где запечка отличается от оригинала ## 2. Единственное место, где запечка отличается от оригинала
XOR идёт по тому, что УЖЕ на экране, то есть результат зависит от фона. XOR идёт по тому, что УЖЕ на экране, то есть результат зависит от фона.
@@ -53,21 +98,18 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре
## 3. Замеры на реальных ассетах ## 3. Замеры на реальных ассетах
Прогон алгоритма по всем кадрам (`SDLPoP/data/KID`, `SDLPoP/data/GUARD`): Прогон алгоритма по правильным наборам (`SDLPoP/data/KID` +
`SDLPoP/data/SHADOW`):
| набор | кадров | макс. габарит | разных цветов | | набор | кадров | макс. габарит | разных цветов |
|---|---:|---|---:| |---|---:|---|---:|
| Кид (chtab_2) | 219 | 56×57 | 44 | | Кид (chtab_2) | 219 | 56×57 | 44 |
| Страж (chtab_5) | 34 | 53×42 | 16 | | SHADOW.DAT (chtab_5 на ур. 12) | 32 | 49×38 | 22 |
| **вместе** | **253** | 57×57 (с учётом +1 px сдвига) | **59** | | **вместе** | **251** | 57×57 (с учётом +1 px сдвига) | **45** |
Всего пикселей во всех кадрах Тени — 96 746. Квантование до N цветов Цветов вместе почти столько же, сколько у одного Кида: SHADOW.DAT сидит на
(оставляем N самых частых, остальные — в ближайший): той же палитре, новых сочетаний XOR почти не даёт. Всего пикселей во всех
кадрах Тени — 91 045.
| палитра | затронуто пикселей | доля | худшая ошибка (евклид RGB) |
|---|---:|---:|---:|
| 16 цветов | 3 483 | 3,60 % | 151 |
| **32 цвета** | **426** | **0,44 %** | 113 |
### Насколько заметна подмена ### Насколько заметна подмена
@@ -79,37 +121,34 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре
- **B** — взвешенный k-means по всем цветам (двигает вообще все); - **B** — взвешенный k-means по всем цветам (двигает вообще все);
- **C** — гибрид: часть слотов под точные частые, остаток — кластеры хвоста. - **C** — гибрид: часть слотов под точные частые, остаток — кластеры хвоста.
| палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая | средняя | | палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая |
|---|---|---:|---:|---:|---:|---:| |---|---|---:|---:|---:|---:|
| 16 | A самые частые | 3 483 (3,60 %) | 2 342 | 257 | 151 | 57,7 | | 16 | A самые частые | 959 (1,05 %) | 569 | 381 | 128 |
| 16 | B k-means | 29 368 (30,4 %) | 1 972 | 256 | 142 | 10,9 | | **16** | **C гибрид 5+11** | 6 984 (7,67 %) | 1 078 | **76** | 114 |
| 16 | C гибрид 10+6 | 7 255 (7,50 %) | 2 146 | **129** | 142 | 33,7 | | 32 | A самые частые | 50 (0,05 %) | 40 | 4 | 113 |
| **32** | **A самые частые** | **426 (0,44 %)** | **205** | **27** | 113 | 46,0 | | 32 | C гибрид 28+4 | 73 (0,08 %) | 58 | **0** | 90 |
| 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 |
Читается так. При 32 цветах стратегия не важна: «сильно» — ровно **27 Читается так. При 32 цветах всё практически идеально: 73 пикселя на 251
пикселей на все 253 кадра** в любой из трёх, это неустранимые одиночки без кадр, грубых промахов нет вовсе. При 16 цветах выбор стратегии виден:
близкого соседа (по 1-3 пикселя на кадр, увидеть нельзя). Перцептивно «самые частые» трогает меньше пикселей (959), но 381 из них уезжает СИЛЬНО;
значимого остаётся 232 пикселя из 96 746, то есть примерно один пиксель на гибрид размазывает ошибку — грубых остаётся 76, то есть примерно **0,3
кадр. При 16 цветах разница уже есть: гибрид вдвое срезает «сильно» (257 пикселя на кадр**.
→ 129), но заметных всё равно две с лишним тысячи.
### Решение: **16 цветов, стратегия C (гибрид 10 + 6)** ### Решение: **16 цветов, стратегия C (гибрид 5 + 11)**
Числа выше — про пиксели, а решает глаз. Отрендерил одни и те же кадры в Числа выше — про пиксели, а решает глаз. Отрендерил одни и те же кадры в
трёх видах (точный цвет / 32 / 16) и сравнил в увеличении ×3: **отличий трёх видах (точный цвет / 32 / 16) и сравнил в увеличении ×3: **отличий
не видно**. Объяснение в самих числах: перцептивно значимых пикселей при не видно**. Объяснение в самих числах: перцептивно значимых пикселей при
16 цветах 2 275 на 253 кадра, это ~9 пикселей на кадр при ~1 000 видимых, 16 цветах гибридом — 1 154 на 251 кадр, это ~4,6 пикселя на кадр при
и они РАССЫПАНЫ по контуру, а не собраны в пятно. ~1 000 видимых, и они РАССЫПАНЫ по контуру, а не собраны в пятно.
Поэтому берём 16, а не 32: экономим блок палитры (пригодится под будущие Поэтому берём 16, а не 32: экономим блок палитры (пригодится под будущие
наборы — принцесса, визирь, мышь), а разница неразличима. наборы — принцесса, визирь, мышь), а разница неразличима.
**Но стратегия обязана быть гибридной.** При 16 цветах «взять самые **Но стратегия обязана быть гибридной.** При 16 цветах «взять самые
частые» вдвое хуже по грубым промахам (257 пикселей против 129), и это частые» впятеро хуже по грубым промахам (381 пиксель против 76), и это
единственное место, где выбор стратегии виден. Гибрид: 10 самых частых единственное место, где выбор стратегии виден. Гибрид: 5 самых частых
берём ТОЧНО, оставшиеся 6 слотов отдаём под взвешенные кластеры хвоста. берём ТОЧНО, оставшиеся 11 слотов отдаём под взвешенные кластеры хвоста.
Если в реальной игре кайма всё же будет резать глаз — переход на 32 цвета Если в реальной игре кайма всё же будет резать глаз — переход на 32 цвета
это одна константа в упаковщике и один блок палитры, данные не меняются. это одна константа в упаковщике и один блок палитры, данные не меняются.
@@ -142,8 +181,8 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре
## 5. Объём ## 5. Объём
253 кадра против 219 у Кида — по страницам EMM примерно как нынешний 251 кадр против 219 у Кида — по страницам EMM примерно как нынешний
набор Кида (28 страниц), плюс пара на кадры стража. При 215 свободных набор Кида (28 страниц), плюс пара на кадры из SHADOW.DAT. При 215 свободных
страницах на старте (memory `sprinter_emm_budget`) это не проблема. страницах на старте (memory `sprinter_emm_budget`) это не проблема.
Кадры смерти («убитый Кид») пока НЕ вырезаем — экономия несколько Кадры смерти («убитый Кид») пока НЕ вырезаем — экономия несколько
@@ -152,6 +191,9 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре
## 6. План работ ## 6. План работ
**Ш0. Довезти SHADOW.DAT.** Его у нас нет вовсе (см. BUG-SHADOW-SET) —
добавить каталог ассетов и правило в Makefile рядом с GUARD/SKEL/VIZIER.
**Ш1. Упаковщик** `toolchain/pop_pack_shadow.py`: прогнать оба набора **Ш1. Упаковщик** `toolchain/pop_pack_shadow.py`: прогнать оба набора
через алгоритм §1, собрать 16-цветную палитру гибридом (10 точных + 6 через алгоритм §1, собрать 16-цветную палитру гибридом (10 точных + 6
кластеров хвоста), выдать кластеров хвоста), выдать
@@ -165,9 +207,9 @@ XOR идёт по тому, что УЖЕ на экране, то есть ре
**Ш3. Отрисовка**: одно место — `pop_cdraw.c:551..568`, где выбирается **Ш3. Отрисовка**: одно место — `pop_cdraw.c:551..568`, где выбирается
`pages`. Сейчас там для соперника берётся `gp`, а для не-боевых кадров `pages`. Сейчас там для соперника берётся `gp`, а для не-боевых кадров
Тени подменяется на `kidp`; добавится ветка «charid == CHARID_1_SHADOW → Тени подменяется на `kidp`; станет «charid == CHARID_1_SHADOW → shadowp»
shadowp», причём обе половины набора (кадры Кида и кадры стража) лежат в БЕЗ подмены: обе половины (кадры Кида и кадры SHADOW.DAT) лежат в ОДНОМ
ОДНОМ атласе Тени, так что подмена становится проще нынешней. атласе Тени, так что ветка становится проще нынешней.
**Ш4. Проверка в MAME**: уровень 4 (Тень у зеркала), уровень 6 (Тень **Ш4. Проверка в MAME**: уровень 4 (Тень у зеркала), уровень 6 (Тень
крадёт зелье), уровень 12 (бой с Тенью — там она в боевых кадрах, то крадёт зелье), уровень 12 (бой с Тенью — там она в боевых кадрах, то
@@ -179,6 +221,7 @@ shadowp», причём обе половины набора (кадры Кид
как `frame + add_frame - 149` (`seg006.c:535`), то есть у нашего как `frame + add_frame - 149` (`seg006.c:535`), то есть у нашего
атласа Тени нумерация двух половин должна совпадать с тем, что уже атласа Тени нумерация двух половин должна совпадать с тем, что уже
делает `pop_frame_tbl_is_guard`. делает `pop_frame_tbl_is_guard`.
2. Цвет стража для Тени: у не-стражей `curr_guard_color = 0` 2. Перекраска Тени НЕ нужна: SHADOW.DAT приходит уже в палитре Кида, а
(`seg002:183`), значит запекать половину «кадры стража» надо с `curr_guard_color` у не-стражей равен 0 (`seg002:183`). Ветку
палитрой стража цвета 0, а не с текущей. `pop_guard_set_palette` для типа 4 звать нельзя — она затрёт палитру
Тени палитрой стража.
+34
View File
@@ -23,6 +23,7 @@
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз | | [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику | | [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** | | [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) | | [LOOSE-ROOM-CHANGE](#loose-room-change) | расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату | порт | закрыт 2026-08-13, кроме [LOOSE-SEAM-ANIM](#loose-seam-anim) |
| [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** | | [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** |
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 | | [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
@@ -836,3 +837,36 @@ if (modif)
`draw_leveldoor`, либо запечка. Проверять на уровне 1 (подземелье, ширина `draw_leveldoor`, либо запечка. Проверять на уровне 1 (подземелье, ширина
39, сдвиг 2) И на уровне 4 (дворец, 48, сдвиг 0) — иначе поймаем только 39, сдвиг 2) И на уровне 4 (дворец, 48, сдвиг 0) — иначе поймаем только
половину. половину.
### <a id="bug-shadow-set"></a>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`).