diff --git a/applications/SprPoP/docs/sdlpop_audit.md b/applications/SprPoP/docs/sdlpop_audit.md new file mode 100644 index 0000000..e1cdcf4 --- /dev/null +++ b/applications/SprPoP/docs/sdlpop_audit.md @@ -0,0 +1,549 @@ +# Аудит расхождений с SDLPoP: Кид, стражи, seqtbl, отрисовка + +> Начат 2026-08-31. КОД НЕ МЕНЯЛСЯ — это только разбор. Задача: найти +> места, где наш движок может вести себя иначе, чем оригинал, и оценить +> вероятность того, что расхождение реально. + +## Как читать + +Ранги вероятности того, что расхождение ЕСТЬ и проявляется в игре: + +| ранг | смысл | +|---|---| +| **А** | гарантированное различие: код объективно разный, эффект понятен | +| **Б** | весьма вероятное: код разный, эффект вероятен, но не доказан | +| **В** | средневероятное: код разный, но эффект может гаситься другим местом | +| **Г** | маловероятное: различие есть в форме, эффект скорее отсутствует | +| **Д** | почти невероятное: сходство подтверждено, остаётся крайний случай | + +Ссылки вида `seg005:114` — строка в `assets/orig/SDLPoP/src/`. Наши +ссылки — `файл:строка` в `src/`. + +## Метод и охват + +Сравниваются НАШИ реализации с оригиналом построчно по функциям. Первый +проход (2026-08-31) охватил: + +* диспетчер `control()` (seg005:251) — целиком; +* `land()` (seg005:114) и `start_fall()` (seg006:1099); +* цепочку смерти: `control_kid` (seg006:1390), `play_kid` (seg006:1348), + `take_hp` (seg006:986), `control()` ветка `alive >= 0`. + +НЕ охвачено первым проходом (список для следующих): + +* интерпретатор `play_seq` и полный набор опкодов seqtbl; +* `frame_table` и модификаторы кадров; +* бой целиком: `control_with_sword`, парирование, `strike`, `hurt_by_sword`; +* ИИ стражей (`guard_ai`), особенности скелета, Тени, Джафара; +* `add_kid_to_objtable`/`add_guard_to_objtable`, порядок слоёв, `clip_char`; +* `check_bumped` — сверен ЧАСТИЧНО (находки 14, 15); `check_grab`, + `in_wall`, `check_bumped_look_left` — нет; +* `do_fall` целиком (проверен только вход). + +--- + +## Симптом, с которого начат аудит + +**Наблюдение (пользователь, 2026-08-31):** идёт бой, за Кидом провал на +этаж. Страж колет на последнем HP, Кид отшатывается назад и падает. +Кид умирает — но на экране он этажом ниже В ПРИСЕДЕ, как после мягкого +приземления. + +**Что говорит код SDLPoP.** Разбор цепочки: + +1. `start_fall` (seg006:1099) ПЕРВЫМ ДЕЛОМ убирает меч + (`Char.sword = sword_0_sheathed`) — для любого падения, независимо от + здоровья. Значит к моменту приземления меч уже в ножнах. +2. `land` (seg005:114) при падении на один ряд выбирает + `seq_63_guard_active_after_fall`, только если `charid >= guard` ИЛИ + меч вынут; иначе — `seq_17_soft_land`, то есть ПРИСЕД. Из-за п.1 для + Кида это всегда присед. +3. `land` НЕ смотрит ни на `alive`, ни на `hitp_curr` вовсе. +4. `control` (seg005:251) при мёртвом персонаже (`alive >= 0`) не + диспетчеризует ничего; он переводит в `seq_71_dying` ТОЛЬКО из + четырёх кадров стойки (15, 166, 158, 171). Присед в этот список не + входит. + +**Вывод:** по букве оригинала мёртвый Кид, застигнутый смертью в полёте, +тоже долетает, приземляется в присед и остаётся в нём — `control` его не +трогает. То есть наблюдаемое, СКОРЕЕ ВСЕГО, воспроизводится и в SDLPoP. + +Гипотеза «hp стал нулевым, поэтому спрятали меч» кодом НЕ подтверждается: +единственное место, где SDLPoP связывает `hitp_curr == 0` с чем-либо, — +`control_kid` (seg006:1395), и там взводится только `Char.alive = 0`. + +**Как проверить окончательно:** прогнать сцену на живом SDLPoP (он +собирается в `assets/orig/SDLPoP/`), где для этого уже есть наши врезки +`POP_TRACE`. До проверки считаю симптом НЕ доказанным расхождением — +ранг **В** (см. находку 5). + +--- + +## Находки первого прохода + +### 1. `land()`: лишний `determine_col()` — ранг **А** + +*Оригинал:* `land` (seg005:114) заканчивается тремя действиями: +`seqtbl_offset_char(seq_id)`, `play_seq()`, `Char.fall_y = 0`. Никакого +пересчёта колонки. + +*У нас:* `pop_map.c:715` (и в ветке смерти `:705`) после `play_seq()` +дополнительно вызывается `determine_col()`. + +*Чем грозит:* `determine_col` пересчитывает `Char.curr_col` из `Char.x`. +Оригинал делает это в другом месте и в другой момент — +`load_fram_det_col` (seg006:0144) перед разбором кадра. Лишний пересчёт +сразу после приземления может дать другую колонку, если `play_seq` +успел сдвинуть `x` первым же `dx`. Колонка — вход для проверок тайла, +пик и коллизий. + +*Замечание:* приём применён у нас системно (`pop_map.c` — 8 вызовов), то +есть это, вероятно, осознанная адаптация, но в `docs/impl_diff.md` она НЕ +записана. Нужно либо обосновать и записать, либо снять. + +### 2. `land()`: обнуляется ещё и `fall_x` — ранг **Б** + +*Оригинал:* в `land` обнуляется ТОЛЬКО `fall_y`, и притом в самом конце, +ПОСЛЕ `play_seq()`. + +*У нас:* `pop_map.c:697` и `:713` обнуляют пару `Char.fall_x = Char.fall_y = 0`, +причём ДО `pop_char_set_seq`/`play_seq`. + +*Чем грозит:* два отличия сразу. Во-первых, `fall_x` в оригинале +переживает приземление — если наш сброс лишний, теряется горизонтальный +импульс, влияющий на последующие кадры. Во-вторых, момент: если +`play_seq` читает `fall_y` (а он читает при обработке своих опкодов +движения), оригинал видит ещё НЕ обнулённое значение, а мы — уже ноль. + +### 3. `land()`: проверка пик до коррекции X — ранг **Б** + +*Оригинал:* сначала (внутри ветки «тайл под ногами не пика») делается +коррекция `Char.x = char_dx_forward(-3)` при `distance_to_edge_weight() < 3`, +и лишь ПОТОМ проверяется падение на пики, причём условие для пики ПОЗАДИ +использует `distance_to_edge_weight() >= 12`. + +*У нас:* `pop_map.c:664` — `fell_on_spikes()` вызывается ПЕРВЫМ, до +коррекции X. + +*Чем грозит:* `distance_to_edge_weight()` считается от `Char.x`, а +коррекция этот `x` меняет на 3 пикселя. У края тайла порядок решает, +попадёт ли персонаж в ветку «пика позади» — то есть умрёт он или нет. + +### 4. Смерть: у нас свой флаг вместо счётчика — ранг **В** + +*Оригинал:* `play_kid` (seg006:1348) после смерти ведёт СЧЁТЧИК +`Char.alive`, и по его значениям запускает музыку смерти (`alive == 6`) и +надпись «Press Button to Continue» (`alive == 7`), причём переход +задерживается, пока играет звук (`check_sound_playing`). + +*У нас:* `pop_ctrl.c` (`ctrl_kid_death`) взводит `pop_kid_dead` — сигнал +главному циклу на респавн; счётчика стадий нет. + +*Чем грозит:* момент респавна и порядок «музыка смерти → сообщение → +рестарт» могут отличаться, особенно если смерть застала персонажа в +длинной анимации (падение). Сюда же относится симптом выше: в оригинале +поза сохраняется, пока крутится счётчик. + +### 5. Мёртвый доигрывает приземление — ранг **В** + +Разобрано выше. Код у нас и в оригинале В ЭТОМ МЕСТЕ совпадает, поэтому +ранг не выше среднего: расхождение может сидеть не в `land`, а в моменте +взведения смерти (находка 4) — тогда оригинал успевает поставить кадр +смерти до приземления, а мы нет. Проверяется прогоном на живом SDLPoP. + +### 6. Диспетчер `control()` — ранг **Д** + +Сверен ветка в ветку (seg005:251 против `pop_ctrl.c:618`): совпадают и +порядок проверок (`bumped/freefall` → меч → charid → кадры), и границы +диапазонов кадров, и обработка мёртвого. Расхождений не видно; остаются +только опциональные `#ifdef`-фиксы SDLPoP, которых у нас нет намеренно. + +### 7. `JMP_IF_FEATHER`: у нас эффект только для Кида — ранг **Б** + +*Оригинал:* опкод `SEQ_JMP_IF_FEATHER` (seg006, play_seq) смотрит ТОЛЬКО +на глобальный `is_feather_fall`. Кто именно проигрывает последовательность, +роли не играет. + +*У нас:* `pop_kid.c:306` добавляет условие `Char.charid != CHARID_0_KID` — +для всех, кроме Кида, ветка «пера» не берётся никогда. + +*Чем грозит:* под зельем медленного падения любой НЕ-Кид, попавший в +последовательности `stepfloat`/`bumpfloat`, у нас пойдёт по обычной ветке +(с уроном), а в оригинале — по парящей. Практически это Тень (charid 1) на +уровне 4-6 и скелет; страж в эти seq попадает редко, но попадает через +`bumpfloat` при отскоке. + +*Замечание:* отличие ОСОЗНАННОЕ (в комментарии сказано «эффект достаётся +только Киду — как и сама физика пера»), но в `docs/impl_diff.md` не +записано, хотя правило проекта этого требует. + +### 8. `start_chompers` отложен до конца `play_seq` — ранг **Б** + +*Оригинал:* опкоды `SEQ_UP`/`SEQ_DOWN` меняют ряд и ТУТ ЖЕ зовут +`start_chompers()` — то есть внутри цикла интерпретатора, до разбора +следующих опкодов. + +*У нас:* `pop_kid.c:318-325` только взводит `chomp_pending`, а сам вызов +происходит после выхода из цикла (`:396`). Причина архитектурная и +описана в коде: seqtbl читается через окно W0, а `start_chompers` лезет в +другое окно. + +*Чем грозит:* два следствия. Во-первых, последовательность с ДВУМЯ +сменами ряда (`UP` `UP`, спуск/подъём по лестнице) в оригинале будит +чомперов в обоих рядах, у нас — только в конечном. Во-вторых, между +`SEQ_UP` и концом цикла успевают отработать `dx`/`dy`/`action`, то есть +оригинал будит чомперов с ДРУГИМИ координатами персонажа. + +Симптом «челюсти не заводятся» уже ловился в этом проекте (memory +`pop_chomper_needs_trigger`), и это место — кандидат в его причины. + +### 9. Набор опкодов seqtbl — ранг **Д** + +Сверены все пятнадцать кодов (`0xF1`..`0xFF`): совпадают и значения, и +семантика, включая проваливание `JMP_IF_FEATHER` в `JMP` и то, что +`SEQ_DIE` — пустышка в обоих движках. Отличия только в находках 7 и 8. + +### 10. Отрисовка: шаги те же, но разнесены — ранг **В** + +*Оригинал:* `add_kid_to_objtable` (seg008:1667) и его двойник для стража — +это строго упорядоченная цепочка: `loadkid`/`loadshad` → +`load_fram_det_col` → `load_frame_to_obj` → `stuck_lower` → +`set_char_collision` → `set_objtile_at_char` → `redraw_at_char` → +`redraw_at_char2` → `clip_char` → `add_objtable`. + +*У нас:* все звенья присутствуют, но распределены по слоям: `clip_char`, +`load_frame_to_obj`, `check_mirror`, брызги — в `pop_cdraw.c`; +`redraw_at_char`/`set_objtile_at_char`/`set_char_collision` — в `pop_bg.c` +(единый проход на всех Char, см. CLAUDE.md). + +*Чем грозит:* сам по себе перенос не ошибка, но ПОРЯДОК внутри цепочки +влияет на результат: `set_char_collision` и `set_objtile_at_char` готовят +данные, которыми пользуются `redraw_at_char` и `clip_char`. Если наш +общий проход выполняет их для ОБОИХ персонажей до отрисовки, а оригинал — +для каждого непосредственно перед его выводом, то при наложении Кида и +стража состояние на момент клипа будет разным. + +*Отдельно:* `stuck_lower` найден только в `pop_cdraw.h` — надо убедиться, +что он реализован, а не только объявлен. Если его нет, персонаж, +застрявший на границе тайла, будет рисоваться на пиксель выше. + +### 11. Порядок вывода Кида и стража — ранг **В** + +*Оригинал:* `draw_people` (seg008:1635) всегда ставит сначала Кида +(`draw_kid`), затем стража (`draw_guard`), а КТО ОКАЖЕТСЯ СВЕРХУ решает +`add_objtable` — таблица объектов упорядочена по позиции тайла. + +*У нас:* по CLAUDE.md порядок задаёт обход тайлов, «кто позже — тот +поверх». Это близко по смыслу, но не тождественно сортировке objtable. + +*Чем грозит:* при наложении персонажей (бой вплотную, страж перед Кидом) +верхний может оказаться другим. Проверять сравнением кадров боя вплотную +с эталонным SDLPoP (метод — memory `pop_pixel_diff_vs_sdlpop`). + +### 12. `hurt_by_sword`: ветка «сбит с уступа» не портирована — ранг **А** + +*Оригинал:* `hurt_by_sword` (seg002:911) при уколе ВООРУЖЁННОГО персонажа +выбирает одну из двух смертей по обстановке ПОЗАДИ: + +* тайл позади не пустой ИЛИ до кромки меньше 4 → `seq_85_stabbed_to_death` + (заколот на месте); +* иначе → `seq_81_kid_pushed_off_ledge` — отдельная последовательность + «убит и сброшен с уступа», которая сама отыгрывает падение замертво. + +*У нас:* `guards.c:1015` — ветки `seq_81` НЕТ вовсе, всегда `seq_85`. +Упрощение ЗАДОКУМЕНТИРОВАНО в комментарии (`guards.c:1011`): ей нужны +тайловые запросы от `Char`, а `pop_map` умеет их только от `Kid`. + +*Чем грозит:* именно тем, что наблюдал пользователь. Заколотый на краю +обрыва Кид в оригинале уходит в собственную анимацию падения с уступа; у +нас он получает «смерть на месте», продолжая при этом висеть в воздухе — +дальше им распоряжается обычная физика падения, и он приземляется этажом +ниже по общим правилам (а с убранным в `start_fall` мечом — в присед, +находка 5). + +*Как проверить:* поставить Кида спиной к обрыву с 1 HP и дать стражу +уколоть. В оригинале — падение замертво (кадры seq_81), у нас — смерть +на месте с последующим отдельным падением. + +### 13. `hurt_by_sword`: прижатие к полу стало безусловным — ранг **А** + +*Оригинал:* `Char.y = y_land[Char.curr_row + 1]` и `Char.fall_y = 0` +выполняются ТОЛЬКО в ветке выжившего удара (seg002:962, рядом с +`seq_74_hit_by_sword`). Смертельные ветки координату не трогают. + +*У нас:* `guards.c` — те же две строки стоят ПОСЛЕ всего `if/else`, то +есть выполняются и при смерти тоже. + +*Чем грозит:* персонажа, убитого в воздухе, мы принудительно ставим на +пол текущего ряда и обнуляем накопленную скорость падения. Дальше физика +обнаруживает, что пола под ним нет, и запускает падение ЗАНОВО — уже без +`fall_y`, то есть с другой высотой и другим исходом приземления. Это +вторая половина механизма из находки 12 и вероятная причина того, что +мёртвый Кид доезжает до нижнего этажа «своим ходом». + +*Как проверить:* тот же сценарий; в отладчике смотреть `Char.y` и +`fall_y` сразу после попадания — оригинал их не меняет. + +### 14. Отскок с мечом: нет `seq_64` — ранг **Б** + +*Оригинал:* при отскоке (`bumped`, seg004:328) живой персонаж с вынутым +мечом получает ОДНУ ИЗ ДВУХ последовательностей по направлению толчка: +толкнули вперёд — `seq_65_bump_forward_with_sword`, отбросило назад — +`seq_64_pushed_back_with_sword`. + +*У нас:* `pop_map.c` знает только `SEQ_65_BUMP_FWD_SWORD` (объявлен на +`:103`, используется на `:2231`); константы и ветки `seq_64` нет вовсе. + +*Чем грозит:* персонаж, отброшенный назад с мечом (страж у стены, Кид в +тесной комнате), проигрывает не ту анимацию — либо ветку без меча, либо +`seq_65`. Кадры разные, а вместе с ними расходятся и смещения `dx` в +последовательности, то есть итоговая позиция после отскока. + +*Как проверить:* бой вплотную к стене, толчок в сторону стены и от неё; +сверять кадры с эталонным прогоном SDLPoP. + +### 15. `check_bumped_look_right`: гейт по направлению — ранг **В** + +В нашей реализации (`pop_map.c:2148`) стоит ранний выход по +`Char.direction` с пометкой «(меча в руке у нас нет)». Пометка означает, +что ветка писалась до появления боя, а оригинал в этом месте учитывает и +меч, и `push_direction` (находка 14). Область `check_bumped_look_left` +не сверялась вовсе — её надо пройти отдельно. + +### 16. `control_with_sword` — ранг **Д** + +Сверен целиком (seg005:964 против `pop_ctrl.c:587`): гейт по `action`, +условие «пол под ногами loose ИЛИ страж видит Кида», пороги дистанции +(90 и −4), `seq_60_turn_with_sword`, ветка «соперник умер» с +`seq_92_put_sword_away`, разделение по `charid`. Совпадает. + +Отдельно отмечу: в оригинале сравнение дистанции сделано ЗНАКОВО-НЕЯВНО +(приведением к `word`), из-за чего ветка «соперник за спиной» вообще +достижима. У нас то же самое выражено явными знаковыми сравнениями — и +диапазоны совпадают, включая «вплотную за спиной» (−4..−1), где обе +реализации ведут бой, а не разворачиваются. + +### 17. `parry` — ранг **Д** + +Сверен целиком (seg005:1064 против `pop_ctrl.c:513`): список кадров +стойки, порог 32 для не-Кида, обработка кадров соперника (151/152/162, +особый случай 153 с отложенным `play_seq`), ветка стража по кадру 152, +ветка `frame_167_blocked` с `seq_61`, сброс автоповтора `control_up`. +Совпадает вплоть до порядка условий. + +### 18. `check_hurting`: звук «меч в движении» в других условиях — ранг **Б** + +*Оригинал:* звук 11 играется в САМОМ КОНЦЕ `check_hurting` (seg002) и +только при трёх условиях сразу: направление персонажа не `none`, его кадр +— укол (154), а соперник при этом НЕ парирует и НЕ ранен. Первое условие +добавлено в SDLPoP специально против зацикливания звука. + +*У нас:* `guards.c:1068` — звук играется в начале ветки укола, +безусловно, ещё до того, как определено попадание. + +*Чем грозит:* лишние срабатывания в двух ситуациях, где оригинал молчит — +когда удар парирован и когда он попал. То есть в самой частой части боя +звук звучит чаще, чем должен. Плюс отсутствует защита от зацикливания +при `direction == none`. + +*Как проверить:* бой с парирующим стражем; считать срабатывания звука 11 +на серии ударов и сравнить с эталонным прогоном SDLPoP. + +### 19. Остальной бой сверен — ранг **Д** + +Прочитаны целиком и совпадают: + +* `swordfight` (seg005:998) — включая ветку кадра 161, `sword_strike`, + побочные эффекты уборки меча (`offguard`, `guard_refrac`, + `holding_sword`), разделение `seq_93`/`seq_92`/`seq_87` по `charid` и + хвост (`parry` / `forward_with_sword` / `back_with_sword`); +* `sword_strike` (seg005:1037) — список кадров, выбор `seq_75`/`seq_58`, + `seq_66` после парирования, сброс автоповтора; +* `check_sword_hurt` (seg002:971) — включая ПРИОРИТЕТ СТРАЖА при + одновременном ранении и сброс `Kid.action` в бег, а также + `refractimer` по навыку; +* `check_hurting` в основной части — гейты по мечу, ряду и кадрам, пороги + дистанции (29), `min_hurt_range` 8/12 по мечу соперника, ветка + парирования с `justblocked` и `seq_69`. Единственное расхождение — + звук, находка 18. + +### 20. Стражи: «стена впереди» сужена до одного тайла — ранг **Б** + +*Оригинал:* `guard_follows_kid_down` (seg002:811) и соседние ветки ИИ +спрашивают `wall_type(tile) != 0`. Эта функция (seg006:1626) считает +преградой ПЯТЬ видов тайлов: ворота, верх двери с полом, верх двери, +зеркало, чомпер и собственно стену — с разной стороной блокировки. + +*У нас:* `guards.c:683` и `:685` сравнивают тайл напрямую с `TILE_WALL` +(тип 20). Ворота, верх двери, зеркало и чомпер преградой не считаются. + +*Причина:* `wall_type` реализована у нас (`pop_map.c:504`, таблица на +`:498`), но НЕ экспортирована — в `pop_map.h` её нет, поэтому `guards.c` +до неё не дотягивается. То есть это не пробел в портировании логики, а +следствие границы модулей. + +*Чем грозит:* страж, преследующий упавшего Кида, у нас шагнёт вперёд там, +где оригинал отступает — перед закрытыми воротами, верхом двери, +зеркалом и чомпером. Отсюда возможны и проход стража сквозь препятствие, +и падение туда, куда оригинал его не пускает. + +*Как проверить:* уровень с воротами (например, 3-й) — заманить стража к +закрытым воротам после падения Кида и сравнить, отступает ли он. + +*Замечание:* в `guards.c` таких мест ЧЕТЫРЕ (`:148`, `:683`, `:685`); +одно из них (`:148`) уже перечисляет три тайла вручную, то есть +расхождение частично компенсировано, но не везде одинаково. + +--- + +## Область: столкновение со стенами и падение внутри стены + +Заведена по наблюдению пользователя (2026-08-31): разбег, прыжок сделан +рано, Кид не долетел, врезался в стену и начал падать — но по X он +оказался ВНУТРИ стены и падал частично в ней. + +### 21. Фикс «скольжения сквозь стену» не портирован — ранг **Г** (соответствие ванили) + +*Оригинал:* в `do_fall` (seg005:37) есть блок `FIX_GLIDE_THROUGH_WALL` с +собственным комментарием SDLPoP: «Кид падает сквозь стены после разворота +в беге, особенно в невесомости». Блок опциональный — то есть в ВАНИЛЬНОЙ +игре этот баг ЕСТЬ, а SDLPoP его чинит по желанию. Рядом такие же +опциональные `FIX_JUMP_THROUGH_WALL_ABOVE_GATE` и `FIX_DROP_THROUGH_TAPESTRY`. + +*У нас:* ни один из трёх не портирован — мы намеренно повторяем ваниль. + +*Вывод по симптому:* «падение частично в стене» — с большой вероятностью +ОРИГИНАЛЬНОЕ поведение PoP, а не наша ошибка. Ранг Г означает: различия +с ванилью, скорее всего, нет. Но проверить стоит другое — не ХУЖЕ ли у +нас, чем в ванили (см. находки 22 и 15). + +*Как проверить:* повторить сцену на живом SDLPoP с выключенными фиксами +(они выключаемы в его настройках) и сравнить глубину захода в стену. + +### 22. `do_fall`: наш гард `curr_row <= 2` — ранг **В** + +*Оригинал:* в `do_fall` ветка «достигли нового ряда» выполняется БЕЗ +условия на номер ряда: проверка тайла стены с вызовом выталкивания, затем +`land()` либо переход на ряд ниже. + +*У нас:* `pop_map.c:909` — вся ветка обёрнута в `if (Char.curr_row <= 2)`. +Причина задокументирована (`:768`): наш `get_tile` за нижней кромкой +комнаты отдаёт СТЕНУ как сентинель, тогда как в оригинале там комната +снизу, и без гарда выталкивание срабатывало ложно, смещая падение на тайл. + +*Чем грозит:* гард гасит не только ложные срабатывания. Если персонаж +достиг `curr_row == 3` легитимно (падение между комнатами по вертикали), +у нас не выполнится ни выталкивание из стены, ни `land()`, ни переход +ряда — всё это ляжет на следующий кадр и другую ветку. Именно такая +комбинация (падение у границы комнаты рядом со стеной) даёт кандидата в +причины наблюдения пользователя. + +*Как проверить:* падение вдоль стены точно на стыке комнат по вертикали; +в отладчике смотреть `curr_row`, `Char.x` и факт вызова выталкивания. + +### 23. `bumped_fall` — ранг **Д** + +Сверен (seg004 против `pop_map.c`): откат X на 4 пикселя назад, обнуление +горизонтальной скорости в свободном падении, иначе `seq_45_bumpfall` с +проигрыванием, звук удара. Совпадает; у нас добавлен только флаг «стражи +услышали», который в оригинале ставится внутри звуковой функции. + +**Замечание по области:** глубина отката при столкновении — ровно 4 +пикселя в обоих движках. Если Кид вошёл в стену глубже (а при +недолёте с разбега скорость по X велика), одного отката не хватит ни там, +ни у нас — и дальше всё зависит от того, сработает ли выталкивание из +стены на следующем кадре. У нас его может съесть гард из находки 22. +Это главная зацепка по симптому. + +### 24. `in_wall`: не перезагружается кадр — ранг **Б** + +*Оригинал:* `in_wall` (seg006) после выталкивания персонажа из стены +делает `load_fram_det_col()` — ЗАГРУЖАЕТ КАДР и следом определяет колонку, +затем перечитывает тайл. + +*У нас:* `pop_map.c` (`in_wall`) вызывает только `determine_col()`. +Пороги (`>= 8`), формулы смещения (`6 - d` и `d + 4`), условие по тайлу +впереди и финальное чтение тайла совпадают — расходится только этот шаг. + +*Чем грозит:* после выталкивания данные кадра (картинка, смещения, флаги +— включая «нужен пол» и «чётный пиксель») остаются от позиции ДО +коррекции, а ими пользуются проверки того же кадра: падение, клип, +коллизия. Это ровно область, где наблюдалось падение внутри стены +(находки 21, 22). + +*Как проверить:* недолёт с разбега в стену; в отладчике сравнить `Char.x`, +колонку и поля текущего кадра сразу после выталкивания. + +### 25. Таблицы кадров и seqtbl — ранг **Д** + +`frame_table_kid`, `original_seqtbl` и таблица смещений извлекаются +АВТОМАТИЧЕСКИ из исходников SDLPoP (`tools/pop_extract_kid_data.py` → +`gen/kid_data.h`), поэтому расхождение в данных маловероятно по +построению. Применение тоже сверено: используются все четыре флага кадра +(«нужен пол» 0x40, вес по X 0x1F, «тонкий» 0x20, чётный пиксель 0x80), а +байт клинка маскируется как в оригинале (`& 0x3F`, `pop_kdraw.c:31`). + +Не сверено: старшие два бита байта клинка (номер набора спрайтов) — у +Кида он нулевой, у прочих персонажей стоит проверить отдельно. + +### 26. Полнота автоуправления — ранг **Д** + +Из двенадцати функций `autocontrol_*` оригинала у нас есть одиннадцать. +Отсутствующая — тривиальная обёртка над общей логикой стража; у нас она +встроена в вызывающего. Расхождения нет. + +Не сверены ПОСТРОЧНО тела: `autocontrol_guard_kid_armed`, +`autocontrol_guard_kid_far`, `autocontrol_shadow*`, `autocontrol_skeleton`, +`check_grab`, `check_bumped_look_left`, `back_with_sword`, +`forward_with_sword`. + +--- + +## ИТОГ АУДИТА + +Проверено 26 позиций за пять проходов. + +| ранг | находки | суть | +|---|---|---| +| **А** | 1, 12, 13 | лишний пересчёт колонки в `land`; нет ветки «убит и сброшен с уступа»; безусловное прижатие к полу при смерти | +| **Б** | 2, 3, 7, 8, 14, 18, 20, 24 | `fall_x` и момент сброса; порядок «пики / коррекция X»; перо только для Кида; отложенные чомперы; нет `seq_64`; звук удара; «стена» сужена до одного тайла; нет перезагрузки кадра в `in_wall` | +| **В** | 4, 5, 10, 11, 15, 22 | флаг смерти вместо счётчика стадий; мёртвый доигрывает приземление; разнесённая цепочка отрисовки; порядок Кид/страж; гейт в `check_bumped_look_right`; гард `curr_row <= 2` в `do_fall` | +| **Г** | 21 | опциональные фиксы SDLPoP не портированы — соответствие ванили | +| **Д** | 6, 9, 16, 17, 19, 23, 25, 26 | сверено и совпадает | + +### Три узла, вокруг которых группируются расхождения + +1. **Смерть при активной физике** (1, 12, 13, 4, 5). Здесь все находки + ранга А. Общая причина: у нас смерть — это флаг, а физика продолжает + работать с персонажем как с живым. +2. **Границы модулей** (12, 20, 24). `pop_map` не отдаёт наружу то, что + нужно `guards.c` и работе с произвольным `Char`: тайловые запросы от + `Char`, `wall_type`, загрузку кадра. Ветки упрощались не по логике, а + по доступности функций. +3. **Момент побочных действий** (2, 8, 18, 24). Делаем то же самое, но + раньше или позже оригинала: сброс скорости, побудка чомперов, звук, + перезагрузка кадра. По отдельности мелочь, вместе — сдвиг состояния + на кадр. + +### Что делать дальше + +1. Проверить находки А и Б в MAME по сценариям из их описаний — начиная с + 12/13 (смерть на краю) и 24 (выталкивание из стены). +2. Те же сцены прогнать на живом SDLPoP: часть наблюдений может оказаться + ванильным поведением (как находка 21). +3. Подтверждённые осознанные отличия записать в `docs/impl_diff.md` — + сейчас там нет ни одного из найденных, хотя правило проекта требует. + + +Порядок по ожидаемой отдаче: + +1. **Бой** — ПРОЙДЕН. Находки: 12, 13 (ранг А), 18 (Б); совпадают + `control_with_sword`, `parry`, `swordfight`, `sword_strike`, + `check_sword_hurt`, `check_hurting` (кроме звука). Не сверены мелочи: + `back_with_sword`, `forward_with_sword`, `check_skel`. +2. **`play_seq` и опкоды** — самая опасная область: ошибка в одном опкоде + меняет все последовательности разом. +3. **Отрисовка** — `add_kid_to_objtable`/`add_guard_to_objtable`, порядок + слоёв, `clip_char`. +4. **`do_fall`/`check_bumped`/`check_grab`** — остаток физики. +5. **ИИ стражей** и особенности скелета/Тени/Джафара.