Разведка железа первым шагом (смена Port_Y между burst-триггерами и
адресация двух страниц в W3), ядро копии с реверсом Y, vflip-блиты для
row-major, gfx_copy_rect экран-экран; в приложении — состояние, единая точка
пересчёта Y, фон через pop_tile.c, зеркальные кадры персонажей, клип.
Побочной задачей ENTER-ROOM-FAST на том же кирпиче.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Момент переключения зелья переворота делаем не перерисовкой комнаты, а двумя
построчными проходами акселератора (A->B с реверсом Y, B->A без): accel
читает ОЗУ-копию, поэтому копируется чистый фон и ложится и в видео, и в
копию приёмника — heal остаётся без изменений. Бюджет 768 burst-строк
(0.2-0.3 логического кадра) против почти миллиона тактов у перерисовки.
Отдельным пунктом: деления HP переезжают из column-major атласов в
const-массивы резидента, что убирает pop_kid_img_blit (334 Б) и делает
страницы kid27/g0 однородными для кэша перевёрнутых кадров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровни 10 и 11 не приносят ни новых тайлов, ни спецсобытий (зелья только
heal / +max HP). Уровень 9 — зелья типа 4 (переворот) в комнатах 7 и 10.
Записан разбор механики оригинала и разбор цены переворота для наших двух
раскладок спрайтов (row-major бесплатно, column-major требует копий).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спецсобытие уровня 8 (SDLPoP seg003:0545/0ABA, seg002:07EB): когда дверь
уровня открыта, а Кид остаётся в комнате 16, через 150 кадров приходит мышь,
пробегает справа налево по верхнему ряду, наступает на кнопку (0,7) —
решётка (0,3) открывается — и убегает.
Ассетов не потребовалось: кадры 186..188 идут по таблице Кида, их спрайты
(images 130..132) уже лежат в kid16.atl, отрисовка подхватывает мышь сама.
- guards.c: pop_check_mouse, autocontrol_mouse, ветки мыши в
autocontrol_opponent / play_guard / check_can_guard_see_kid;
- pop_leveldoor_open стал word, как в оригинале (в него же считается
задержка — байт переполнился бы через 255 кадров);
- leave_guard больше не записывает тень и мышь в данные уровня (seg002:02F5);
- полосы HP у мыши нет (seg000:1159);
- tests-host/t_mouse: 17 проверок, единственный набор с guards.c.
Разгрузка резидента (куча в huge упала до 879 Б): pop_start_level,
find_start_level_door, чит-навигация по комнатам и лейбл номера уехали в
roomtest_cold.c (--bank 8, n_banks = 8). _CODE 25653 -> 24825, куча -> 1707 Б;
кадровый путь не тронут.
Проверено: tests-host 6/6, make size-check OK, живьём в MAME (мышь появилась,
нажала кнопку, решётка поехала вверх, мышь ушла).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L7-FEATHER: тип 3 (уровень 7, комната 1) больше не пустой TODO.
- pop_map.c: pop_feather — счётчик кадров эффекта (POP_FEATHER_FRAMES = 225,
ванильный порог do_timers seg003:0517; привязку к звуку взять неоткуда).
fall_accel — ускорение 1 / потолок 4 (seg006:057C) вместо 3 / 33.
proc_get_object case 3: взвести эффект + зелёная вспышка на 3 кадра.
- pop_kid.c: опкод JMP_IF_FEATHER (0xF7) больше не пропускает адрес
безусловно — под пером прыгает по нему, то есть seqtbl уводит падение и
удар в ветки stepfloat/bumpfloat (плавные кадры, без урона).
- roomtest.c: pop_flash_red -> pop_flash_color (жёлтый/красный/ЗЕЛЁНЫЙ);
сброс pop_feather в pop_start_level (seg003:189).
- Цвет пузырька по ТИПУ зелья (seg008:652), чего у нас не было вовсе:
3/4 зелёный, 5/6 СИНИЙ, остальные красный. Mono-блиттера с параметром
цвета в libbgi нет, поэтому цвет запекается при упаковке: pop_pack_bg.py
кладёт те же 7 кадров ещё дважды (id 30..36 зелёные, 40..46 синие),
pop_potion_draw выбирает набор. Атлас 23 -> 37 спрайтов (+423 Б).
- Синее зелье «−HP» приведено к оригиналу (seg006:1892): своей вспышки не
ставит (красный кадр даёт общий flash_if_hurt — иначе экран красился
дважды), а на уровне зелий забирает ПОЛОВИНУ запаса HP.
Такое зелье стоит уже на пройденном уровне 2 (комната 13, тайл (1,3)),
а также ур.8 комн.2 и весь ур.15 — до сих пор оно было красным.
Тесты: phys_feather_fall_is_slow_and_harmless (обычное падение с двух рядов
разгоняется и стоит HP, под пером скорость <= 4 и HP целое); tests-host
5/5 (1727 в [phys]), size-check OK.
Расхождения с ванилью — в docs/impl_diff.md (перо ловит только Кида;
синее зелье без своей вспышки).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 7, комната 14: спуск с ряда 0 на ряд 2 через зацеп (повис на кромке
кнопки (0,2), отпустил, взялся за (2,2)) не работал ни при какой фазе и
задержке — Кид пролетал мимо и разгонялся до fall_y = 33, после чего окно
зацепа закрыто уже по скорости.
Корень: do_fall не давал curr_row выйти за 2, а check_grab целится в тайл
ряда curr_row-1 — то есть ряд 2 своей комнаты становится целью только при
curr_row == 3. В оригинале (seg005:0030) inc_curr_row безусловный, а
get_tile для ряда 3 уходит по links.down (find_room_of_tile, seg006:005D).
- pop_map.c do_fall: inc_curr_row теперь 2 -> 3; ветка «достиг y_land»
гейтится по curr_row <= 2 (при ряде 3 ни in_wall, ни land звать нельзя —
тайлов своей комнаты там нет, а до y_land[4] дело не доходит: комнату
меняет check_leave_below на y >= 211).
- pop_map.c check_action: восстановлена ветка ACT_MIDAIR (кадры 102..105,
seg006:0619) — первые четыре кадра падения, где fall_y ещё не разогнан,
зацеп не работал вовсе. Отсюда же «иногда цепляется, иногда нет».
- tests-host/t_grab.c: регресс grab_below_room_edge_window_exists — сцена
комнаты 14 + переход в 15 по pop_fell_out. До фикса ни одного зацепа,
после — окно из 8 фаз X при задержках 0..5 кадров; проверяется и
играбельность (зацеп при «отпустил и сразу зажал Shift»).
- roomtest.c + pop_guard.h: чит-навигация ставит Кида на верхний ряд в
комнате 14 уровня 7 (GRAB_DOWN_LEVEL/ROOM) — снизу этот спуск не проверить.
Общее правило (снизу вверх) не тронуто: в верхнем ряду пола чаще нет и Кид
проваливается сразу после телепорта.
Проверено: tests-host 5/5 (55 в [grab]), size-check OK, живьём в MAME —
Кид повис на кромке (2,2): frame=91 y=55 row=0 room=15.
Доски: GRAB-BELOW-ROOM в BUGS_CLOSED, GRAB-KBD-TIMING в BUGS_OPEN (отложен
пользователем до готовности всех уровней), план L7-FEATHER в TASKS_OPEN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Всё проверено пользователем в MAME.
1. Переход 6 -> 7 падением. leave_room (seg002:0504) для «вниз» на уровне 6
из комнаты 1 даёт особый результат −2, главный цикл (seg000:0893) читает
его как ++next_level; вход на 7-м (set_start_pos, seg003:0196) ставит Кида
в комнату 17 и сразу переводит экран на комнату под ней. Проверка ОБЯЗАНА
идти раньше связи вниз: у комнаты 1 сосед снизу есть (шахта, комната 3).
2. BUG-BALCONY-RIGHT: правая половина портала не рисовалась. id 12 стоял в
FORE_ENV_IDS (в tile_table он fore_id ЗЕЛЬЯ), но зелье рисуется из
chtab_1, а в chtab_6 под этим номером — правая часть арки балкона 32x62,
идущая в backtable. Спрайт уезжал в fore-атлас, env-страница получала
дырку (idx 12: w=0 h=0). Найдено трассой POP_TRACE_BT + чтением каталога
.atl; дифф комнаты с оригиналом 1151 -> 375.
3. BUG-BELOWROW-WALL: жёлтые треугольники в шахте падения. load_rowbelow
(seg008:368) при отсутствующей комнате снизу подставляет tiles_0_empty
колонкам 1..9 и tiles_20_wall только левому краю; у нас стеной забивались
все 11 позиций.
4. BUG-MOB-MULTIROOM: честный mob_down_a_row (seg007:1387) — кусок переходит
в комнату снизу и летит дальше. Плита (1,7) комнаты 6 пролетает шахту
комнаты 7 и жмёт кнопку (2,7) комнаты 11, открывая ворота комнаты 18.
5. BUG-MOB-STALE-PAGE: чистка следа куска стояла под гейтом «в отрисованной
комнате» — улетевший вниз оставлял себя на одной из страниц навсегда.
6. BUG-CHAR-STALE-PAGE: тень застывала на двух страницах в РАЗНЫХ позах.
Прямоугольник и valid пишутся внутри `if (w && h)`, а снимок состояния
брался безусловно — при нулевом спрайте слот залипал «тихим». Остаток
(почему спрайт нулевой) заведён как SPRITE-ZERO-W0 в BUGS_OPEN.
NEXT_SESSION переписан: следующая задача — зацеп за край пола в свободном
падении (вход на уровень 7). tests-host 5/5, size-check чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. L6-SHADOW (seg002:0090 + seg002:1064). Тень встаёт при каждом входе в
комнату 1 уровня 6 и делает ОДИН осторожный шаг (Shift+вперёд) в тот
кадр, когда Кид прыгает к решётке (Kid.frame == 43, Kid.x < 128): встаёт
на closer (1,1) и роняет решётку (1,2), за порожек которой Кид цепляется.
do_init_shad переписан под таблицы init_shad_5/6 — первые 7 полей Char,
как memcpy(&Char, source, 7) у оригинала.
Проверено в MAME: «Кид прыгнул, зацепился, Тень сделал шаг, решётка
упала». Тем самым закрыт остаток GUARD-PHYS — нажатие плиты НЕ-Кидом
работает.
2. BUG-BELOWROW-WALL. При отсутствующей комнате снизу load_rowbelow
(seg008:368) подставляет tiles_0_empty колонкам 1..9 и tiles_20_wall
только левому краю; у нас стеной забивались все 11 позиций, и её
topright лез жёлтыми треугольниками под пустые тайлы нижнего ряда
(видно в шахте падения, комната 3).
Комнаты 1 и 3 уровня 6 сверены с картой SDLPoP: расхождения только силуэты
персонажей. tests-host 5/5, size-check чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 5, комната 12, тайл (0,9): чёрная дыра вместо арки. У doortop нет
своей базы (base_id 0), и рядом с lattice_down оригинал рисует пару одним
спрайтом 6, опущенным на 3 px. Ветки у нас не было.
Правка нужна В ДВУХ местах: pop_room.c (сама отрисовка) и
toolchain/render_room.py — pop_pack_bg.py собирает атлас по обходу
render_room, без этого спрайт 6 не попал бы в pop_env*.atl.
Найдено сверкой с оригиналом: prince megahit 5 --screenshot-level даёт карту
уровня, снимок MAME совмещается с клеткой комнаты, разница по яркости
показала расхождение ровно в (0,9) — 1892 пикселя до фикса, 995 после
(остаток = Кид, факел, статус-полоса). Что рисует оригинал в тайле,
показала трасса POP_TRACE_BT в add_backtable: id=6 32x63 рядом с id=85 32x4.
Методика записана в BUGS_CLOSED.md и memory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три бага одной отладочной сессии, все проверены в MAME пользователем.
1. SHADOW-FIGHT-L5. Кид доставал меч, а тень вставала в боевую стойку и
рисовалась спрайтами стража. В pop_check_can_guard_see_kid первым
множителем стояло `Guard.charid != 0`, тогда как оригинал (seg003:702)
пишет `Guard.charid != charid_1_shadow || current_level == 12`: тень
боевая только на 12-м уровне. Оба симптома шли из одной ветки
control_standing (seg005:352). Возвращён и пропущенный множитель
`Guard.direction != dir_56_none`.
2. SEAM-BUTTON-STALE. Кнопка в шве не меняла вид ни при нажатии, ни при
отжатии. Сигнатура редроя шва сравнивала room_modif, а у кнопки modif —
это индекс LINKLOC, константа уровня: нажатие живёт в doorlinks2, и в
сигнатуру не приходило никогда. seam_row_sig подмешивает бит нажатости.
Оригинал шов в этом случае не перерисовывает вовсе — расхождение
осознанное, записано как D-2 в docs/impl_diff.md.
3. BUG-POTION-STRIPE. Синяя лента паласа пропадала на месте выпитого зелья
и не возвращалась даже после перезахода в комнату. У оригинала
curr_room_tiles — сама таблица уровня, у нас fg живёт в двух местах, и
страницу правил главный цикл уже после pop_process_trobs. В этот зазор
trob зелья крутил фазу пузырька поверх обнулённого модификатора:
bubble_next_frame(0) = 1, а для пола в паласе modif 1 означает «узор не
рисовать» (seg008:499). do_pickup теперь правит страницу уровня сразу.
Тем же лечится меч: animate_sword делал `--mod[tp]` из нуля.
tests-host 5/5 (добавлена заглушка pop_level_set_tile), size-check чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом: Кид встаёт на плиту, которая физически в СОСЕДНЕЙ комнате (стоя в
шве, curr_col = −1), и вместо одних ворот открываются двое — на уровне 5
плита комнаты 11 открывала и нижние ворота комнаты 24, и верхние, хотя
связана только с нижними.
Причина. check_press читал тайл через get_tile (тот резолвит шов в
соседнюю комнату), а комнату и tilepos для trigger_button брал из
координат персонажа: (своя комната, row*10 + curr_col). При curr_col = −1
это tilepos 9 СВОЕЙ комнаты — там стена с bg = 0, то есть индекс цепочки
LINKLOC 0. Цепочка от нуля в данных уровня 5 ведёт на ДВА тайла: нижние
ворота (link[0], next=1) и верхние (link[1]) — ровно то, что наблюдалось.
Настоящая кнопка имеет индекс 9 и одну цель.
В оригинале этого нет по построению: get_tile зовёт find_room_of_tile
(seg006:005D) и ПЕРЕСТАВЛЯЕТ curr_room/curr_tilepos, а trigger_button
работает уже с ними. У нас резолв комнаты жил только внутри get_tile, а
наружу не отдавался.
Фикс: tile_room_of(col,row) — комната и tilepos клетки с учётом швов (по
образцу gate_modif, который так делал давно), check_press зовёт
trigger_button с резолвнутыми room/tilepos. Ветка loose там же оставлена
на координатах персонажа: make_loose_fall работает с g_fg своей комнаты.
Два других вызова trigger_button (зацеп за кромку, севшая на кнопку плита)
правки не требуют — оба ограничены колонками 0..9 своей комнаты.
Заведён GATE-FORE-KID (BUGS_OPEN.md): Кид, стоящий В ПРОЁМЕ ворот, виден
поверх решётки — у нас портирован только шовный случай окклюзии, а
draw_tile_fore (seg008:0D15) рисует решётку поверх персонажа и внутри
комнаты. Чинить в fore-слое отдельно, он горячий.
tests-host 5/5, make size-check OK. Банк 3: 10648 -> 10828 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт seg002: check_shadow (0064), do_init_shad (0000), do_auto_moves (1089),
autocontrol_shadow_level5 (1157) + таблица shad_drink_move (data.h:866).
Механика. При входе в комнату 24 уровня 5, если зелье (кол 3, ряд 0) ещё
на месте, слот соперника занимает ТЕНЬ — и занимает его ВМЕСТО стража:
enter_guard в этой комнате оригинал не зовёт НИКОГДА (в данных страж есть,
guards_tile[23] = 8, но check_shadow уходит в return до него — в том числе
когда зелье уже выпито). Поэтому pop_check_shadow вернула 1 = «событие
обработано, стража не поднимать», и вызов стоит ПЕРЕД pop_guard_enter.
Тень стоит, пока не откроются ворота комнаты (openness >= 80), затем
проигрывает ЗАПИСЬ ходов: подойти, взять, выпить, развернуться, уйти за
левый край. do_auto_moves — тот же движок, что у демо-режима заставки;
тонкость выбора записи (при достигнутом времени индекс сдвигается, а ход
берётся из ещё не сдвинутой записи) портирована дословно.
Три вещи, найденные по дороге:
1. ГЕЙТ ВЫКЛЮЧЕННОГО ПЕРСОНАЖА. play_guard_frame (seg000:1248) начинается
с `if (Guard.direction != dir_56_none)`, и clear_char выключает
персонажа именно так — он не трогает charid. У нас гейт был только по
charid, поэтому ушедшая за край тень продолжала тикать: do_auto_moves за
концом таблицы отдаёт последнюю запись {0x31,1} = «вперёд», тень
разворачивалась, вбегала обратно, пробегала верхний ряд и падала в проём
(наблюдение пользователя в MAME). Гейт добавлен и в pop_guard_tick, и в
pop_guard_phys_tick.
2. ЭФФЕКТ ЗЕЛЬЯ — ТОЛЬКО КИДУ. proc_get_object (seg006:16CB) начинается с
`if (Char.charid != charid_0_kid || ...) return`: предмет забирает любой
персонаж, а эффект достаётся только Киду. На этом стоит всё спецсобытие
— тень пьёт, зелье исчезает и не достаётся никому. Без проверки
hitp_delta уходил бы Киду, то есть тень его ЛЕЧИЛА бы.
3. ВЕТКА ТЕНИ в check_guard_fallout (seg002:0241): исчезает, только если
реально падает (action == 4). Была помечена в коде как «появится с
L3-SKEL» — на самом деле она про тень.
Чит ROOMNAV: телепорт ставил Кида в первый попавшийся пол, то есть почти
всегда (0,0). Теперь ищет пол С МАКСИМАЛЬНЫМ Y (ряд 2 -> 1 -> 0, по
просьбе пользователя): в комнате 24 старая посадка была вплотную к тени, и
вместо сцены кражи начиналась схватка, которой там быть не должно.
Отладочный тумблер POP_DBG_SHADOW_NOWAIT (pop_tune.h, по умолчанию 0) —
посмотреть сцену, не проходя уровень до кнопки.
Проверено в MAME (уровень 5, комната 24): при закрытых воротах тень стоит
за левым краем и не видна; с тумблером — подошла, выпила (зелье исчезло),
развернулась, ушла за край, выключилась и НЕ вернулась. tests-host 5/5,
make size-check OK. Цена: резидент +102 Б (24637 -> 24739), банк 1
+368 Б, банк 3 +11 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прогон читом ROOMNAV: телепорт в комнату 4 сразу после нажатия кнопки —
зеркала нет вовсе (тайл ставится в момент, когда дверь ДОРИСОВАЛА открытие,
43 тика анимации, телепорт успевает раньше); обычный вход в комнату —
зеркало на месте и непроходимо, кроме правильного прыжка, то есть коллизия
его видит. Сценария «постановка при игроке в комнате» в реальном
прохождении нет: дверь выхода стоит не в комнате зеркала, а при входе
комната и снимок room_fg берутся из данных уровня разом.
Решение пользователя: телепорт по комнатам — отладочный режим, чинить нечего.
Запись целиком уехала в BUGS_CLOSED.md вместе с протоколом и с указанием, чем
лечить, если симптом всё же всплывёт на моде/уровне, где кнопка и зеркало в
одной комнате.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Доска отставала от кода на три задачи — планировать по ней было нельзя.
Сверка проведена грепом по исходникам, а не по записям:
- L4-MIRROR ЗАКРЫТА: шаги 1-5 сделаны и проверены пользователем в MAME
(зеркало в атласе, постановка тайла, отражение, прыжок сквозь зеркало с
рождением тени, левый клип тени). Протокол с разбором решений — в архиве;
- L3-CHOMP и L3-SKEL закрыты ещё 2026-08-08/07 (коммиты dc0bd47, 4d4323f,
db4106a, 1461ed5), на доске значились как предстоящие;
- тайлсет palace (шаг 2 levels_plan) в коде есть целиком — pop_bg_load(type),
pal_*.atl, дворцовый wall_pattern, решётки 25-29 и в tile_table, и в
коллизии (tile_is_floor совпадает с seg006:0628);
- в GUARD-PHYS остаток пересобран по факту: check_chomped_guard сделан,
скелет в check_guard_fallout сделан, ветки ТЕНИ нет — она уехала в L5-SHADOW.
Приёмки: по решению пользователя уровни 1-4 приняты SMOKE-тестами, полные
обходы всех комнат делаются по готовности ВСЕХ уровней — L3-PASS/L4-PASS как
отдельные задачи отменены, вместо них политика приёмок в архиве.
Новая цель — уровень 5. Инвентарь res2005.bin: НИ ОДНОГО нового тайла, всё
портировано на уровнях 1-4. Единственная новая механика — спецсобытие «тень
крадёт зелье» (комната 24): заведена задача L5-SHADOW с портом по SDLPoP
(check_shadow / do_init_shad / do_auto_moves + shad_drink_move /
autocontrol_shadow_level5 + ветка тени в check_guard_fallout), включая
готовые константы и то, что у нас уже есть под это.
Заведён MIRROR-FG-STALE (низкий): place_mirror пишет тайл в данные уровня, но
не в снимок room_fg, по которому работает коллизия — если зеркало поставлено,
пока игрок В комнате 4, оно невидимо для коллизии (тень не родится). В
обычном прохождении недостижимо: дверь выхода в другой комнате. Записан
точный сценарий воспроизведения читом ROOMNAV и фикс на несколько строк.
Правило «в _OPEN только незакрытое» теперь выполняется буквально:
- bug_list.md → BUGS_OPEN.md, bug_closed.md → BUGS_CLOSED.md (ссылки
обновлены во всех документах и в комментарии pop_trob.c);
- из TASKS_OPEN убраны блоки закрытых задач (L3-CHOMP, L3-SKEL, L3-PASS,
L4-MIRROR, DRAW-CHAR), справка по связности комнат уехала в архив;
- из BUGS_OPEN убраны 8 строк таблицы закрытых багов, закрытый T-2 (уехал в
BUGS_CLOSED) и раздел «уровень 3 — неначатые задачи» (обе записи закрыты);
сводная таблица пересобрана по реально открытым записям.
Все внутренние ссылки проверены скриптом: битых якорей 0. make size-check
OK (65 программ), tests-host 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Чтобы следующая сессия не начала задачу заново: библиотечная часть закрыта
(полный набор блочных AND/OR/XOR/NOT в libbgi, tests/accop 10/10), а вид
тени отложен по решению пользователя — со ссылкой на docs/shadow_render.md.
Исходная постановка оставлена ниже как справка.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NOT. У акселератора нет режима «инвертировать буфер»: буфер меняется
только на ЧТЕНИИ и только опкодами AND/OR/XOR (HL) (драйвер MAME,
update_accel_buffer), а `CPL` автомат вообще не распознаёт — инвертируется
регистр CPU, не буфер. Зато ~src = src XOR #FF, поэтому NOT собирается из
уже имеющегося: буфер := src, XOR-burst по константному блоку единиц
(common/_bgi_ones256.c), запись. Ядра _bgi_blit_{rows,cols}_not_raw.c;
между burst'ами меняется HL, поэтому каждая смена — под СТОПом (иначе fetch
операнда перезапустит burst). В колоночном варианте второй OUT Port_Y не
нужен: op-burst идёт по блоку единиц горизонтально, а Port_Y шагает только
вертикальный.
Наружу — тем же op-параметром (GFX_OP_NOT), диспетчер один раз на вызов, в
цикл по полосам/колонкам не заходит. putimage лишился попиксельного пути
ЦЕЛИКОМ: все пять операций BGI идут через акселератор и клиппируются
одинаково. tests/accop дополнен T9/T10 (NOT строками и колонками) —
10/10 PASS в MAME; tests/bgi_img P1/P2 по-прежнему PASS.
tests/convbench (новый) — замер побайтной конвертации атласа
«прозрачный #FF -> 0x00» (источник для XOR-блита): 145.3 такта/байт, то
есть 2.5× от 59 номинальных T-states цикла. 0.11 с на страницу 16 КБ,
3.2 с на все 28 страниц, 1.3 с по фактическому объёму данных (186 КБ).
applications/PoP/docs/shadow_render.md — вид Тени (два блиттера OR+XOR)
ОТЛОЖЕН по решению пользователя: пока рисуем обычной копией атласами Кида.
В документе собрано, почему в лоб не выходит (XOR несовместим с
прозрачностью #FF; операция читает ОЗУ-копию, поэтому два прохода
оригинала вырождаются в один XOR — нужен однопроходный композит
s | bg ^ s(сдвиг)), замер стоимости источника и четыре варианта. Ключ к
выбору — список кадров, которыми тень реально пользуется; снимать по факту,
когда пойдут уровни 5/6/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Одним файлом: состояние репозитория (11 коммитов дня с хешами), состояние
окружения (образ MAME на уровне 4; как пересобрать SDLPoP с -g без
pkg-config — нужен -std=gnu99, иначе прячется strncasecmp; неприбранный
диагностический fprintf DBGMIRROR), задача на завтра и открытые пункты.
Задача на завтра — XOR/OR-блит через акселератор. Записано всё, что
установлено замером: тень уровня 4 рисуется ДВУМЯ блитами одного спрайта
Кида (seg008:1602, blitters_2_or + blitters_3_xor со сдвигом на пиксель),
обе сущности идут из chtab=2 — различие только в блиттере. Механизм на
Sprinter из accelerator_doc.txt: операцию задаёт опкод CPU между
триггерами, цена «байт / 7 МГц», операция ортогональна направлению.
Открытый вопрос честно помечен: акселератор XOR-ит ИНДЕКСЫ, а SDLPoP — RGB;
как это ляжет на нашу перепакованную палитру, надо увидеть.
Отдельным разделом — грабли дня, чтобы не повторять: не оценивать железо по
своей же memory-заметке (ошибся с accel, поправил пользователь); lldb через
FIFO ненадёжен, вместо него fprintf прямо в SDLPoP; make без hdd не
обновляет образ MAME.
Ссылка на файл добавлена в roomtest/CLAUDE.md, который грузится сам.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено вопросом пользователя. У нас fore-проход зовёт каждый рисующий по
своему прямоугольнику, поэтому тайл, накрытый двумя объектами, рисуется
два раза (Кид+отражение — всегда, Кид+соперник — весь ближний бой).
В оригинале дубль невозможен по построению: redraw_at_char (seg003:0427)
только ПОМЕЧАЕТ тайлы, а set_redraw_fore (seg007:0550) —
redraw_frames_fore[tilepos] = frames, присваивание. Единственный обход
тайлов рисует передний кусок один раз.
Картинку не портит (прозрачный блит идемпотентен), тратит такты в самом
горячем месте. Запись требует СНАЧАЛА замера: окно клипа вместо перебора
тайлов в своё время дало 3.2x, и переход на пометки может часть вернуть.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом (скриншоты пользователя, ур. 4): у отражения видны ноги ниже
нижней кромки зеркала, а на прыжке — голова и руки выше верхней. В
оригинале и то и другое скрыто.
Причина: над отражением не шёл fore-проход. В SDLPoP отражение уходит в
objtable (add_objtable(4), seg003:0798), и передний слой тайлов накрывает
его на общих основаниях — у зеркала это fore_id 77, ПЕРЕДНЯЯ ЧАСТЬ АРКИ.
Именно она прячет всё, что вылезло из проёма. Клип obj_clip_top этого не
делает и делать не может: при curr_row = 0 он равен y_clip[1] = 3, то есть
режет только по верху поля.
У нас отражение рисуется мимо конвейера персонажей (сокращённый путь, как
и в оригинале), поэтому проход надо звать руками — ровно как это делает
pop_char_fore для слотов: pop_fore_set_clip по нарисованному
прямоугольнику + pop_fore_over_char(&Char, ...) с ЛОГИЧЕСКОЙ x кадра.
Порядок сходится сам: pop_check_mirror стоит до отрисовки персонажей, так
что получается отражение -> его fore -> Кид -> его fore, и каждый ставит
своё окно клипа.
Банк 4: 9590 -> 9770. tests-host: все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
libbgi: новая gfx_blit_cols_part_wx — обрезка СЛЕВА (skipw) в дополнение к
верхней (skip/rows) и правой (maxw). По подсказке пользователя сделано
примитивом, а не обходным путём «нарисовать и вернуть фон поверх лишнего»:
для column-major левая обрезка стоит ровно столько же, сколько правая —
колонка это непрерывный кусок ОЗУ, меняется стартовая колонка источника и
экранная X. Внутри это уже было (так клипается левый край экрана), наружу
не было выведено. Полное тело блита колонками переехало туда,
gfx_blit_cols_part_w стала обёрткой (skipw=0). size-check: OK, роста нет
(62 программы, -22..-33 Б на пользователей блита).
Шаг 5 — клип ТЕНИ (seg008:1699): на уровне зеркала она показывается только
СПРАВА от него, obj_clip_left = 137 + (mirror_column-4)*32.
Шаг 3 — ОТРАЖЕНИЕ (check_mirror, seg003:0798): пока Кид стоит на тайле
зеркала, каждый кадр рисуется его зеркальная копия с клипом
left = (curr_col<<5)+9 и top = y_clip[curr_row+1]. Отдельной функцией
pop_mirror_draw, а НЕ третьим слотом Char — это структура самого оригинала:
отражение идёт сокращённым путём load_frame_to_obj + add_objtable(4), без
клинка, брызг, fore-прохода и пропуска кадра; гейтить всё это в общем теле
значило бы добавить ветки в самый горячий путь. Свой heal (pop_mirror_heal)
рядом с pop_char_heal, в skip-маске не участвует.
tests-host: заглушки pop_mirror_draw/pop_mirror_heal (pop_map теперь на них
ссылается), все 5 наборов прошли.
Цена: _CODE 24501 -> 24629, банк 3 10551, банк 4 8267 -> 9590.
НЕ ПРОВЕРЕНО В MAME.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт seg003:0798..08A9 + seg004:0239 + seg002:081D/1131 + seg006:1945.
- is_obstacle (pop_map.c): ветка зеркала — Кид, кадры бегового прыжка
39..43, направление ВЛЕВО -> modif = 0x56, pop_jumped_mirror = -1,
препятствия нет (пролетает насквозь).
- mirror_image / jump_through_mirror / pop_check_mirror (pop_map.c):
отражённый Char уходит в слот Guard как CHARID_1_SHADOW, guardhp =
hitp_max, у Кида hitp_curr = 1. savekid НЕ делается — как в оригинале,
отражается только копия. Полосы HP перерисует pop_hp_draw сам.
- pop_check_mirror() зовётся из главного цикла ПЕРЕД отрисовкой персонажей
(в оригинале — первая строка draw_people, seg008:228A).
- autocontrol_shadow + autocontrol_shadow_level4 + clear_char (guards.c):
тень идёт СВОЕЙ веткой целиком, к стражьему ИИ не сводится — на уровне 4
она не дерётся, а бежит влево и при x < 80 исчезает.
АТЛАС ТЕНИ — вскрылось при чтении seg006:0532. Тень вне боевых кадров
150..189 ходит по таблице КИДА, и image оттуда индексирует спрайты Кида,
а не стража. Выбор атласа в pop_cdraw шёл по СЛОТУ, то есть тень
рисовалась бы спрайтами стража. Условие вынесено в
pop_frame_tbl_is_guard() (pop_kid.c) — его теперь читают и load_frame, и
отрисовка, разъехаться не могут. Заодно из pop_load_fram_det_col выделен
pop_load_frame() без determine_col: jump_through_mirror берёт ось
отражения из curr_col, и пересчёт колонки по x там был бы вреден.
Константы MIRROR_* и DIR_56_NONE переехали в pop_guard.h — нужны и
постановке тайла (банк 6), и ИИ тени (банк 1).
Звука sound_45_jump_through_mirror в порте нет, пропущен.
Шаг 5 (клип тени слева от зеркала) НЕ сделан и оказался не однострочником:
в pop_cdraw есть клип сверху/снизу/справа, левого нет вовсе — нужен новый
примитив либо срез исходных колонок. Расписано в TASKS_OPEN.
Цена: _CODE +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551,
банк 4 8243 -> 8267. tests-host: все 5 наборов прошли.
НЕ ПРОВЕРЕНО В MAME — сценарий проверки записан в TASKS_OPEN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача заведена активной на доске по решению пользователя (palace был
отложен 2026-08-04, но уровень 4 уже гоняется в MAME и зеркало —
единственное, что мешает пройти его сюжетно). Механика целиком сверена по
SDLPoP, таблица соответствий в TASKS_OPEN.
Шаг 1 — АТЛАС. tile_table[0x0D] = база 75, фронт 77 (наша таблица
совпадает с SDLPoP байт в байт). Тайла 13 НЕТ НИ В ОДНОМ уровне
статически: перебор всех 15 res200N.bin даёт ноль попаданий (санити
разбора: ур.1 без чомпера, ур.3 с 18, ур.4 с паласными 25..29). Значит
render_room эти id не увидит и на месте зеркала был бы чёрный провал —
грабли memory pop_atlas_dynamic_ids. Добавлены MIRROR_ENV_IDS = {75,77} в
pop_pack_bg.py, 77 ещё и в FORE_ENV_IDS. Оба набора переупакованы:
fore 17 -> 18 спрайтов, все страницы EMM в пределах 16 КБ.
Шаг 2 — ПОСТАНОВКА. place_mirror() в pop_trob.c по переходу
pop_leveldoor_open 0/2 -> 1 (условие оригинала, seg007:0457 — иначе тайл
ставился бы заново каждый кадр открытой двери). Пишет тайл 13 в комнату 4,
колонку 4, ряд 0; если комната уже на экране — POP_RD_FLOOR на обе
страницы. Банк 6 3450 -> 3518.
НЕ ПРОВЕРЕНО В MAME: нужно нажать плиту выхода на уровне 4 и дойти до
комнаты 4. Отдельный вопрос к проверке — не устареет ли g_fg коллизии,
если игрок окажется в комнате 4 в момент постановки.
Дальше по плану: 4 (прыжок сквозь + рождение тени), 5 (клип тени),
3 (отражение — косметика, самое дорогое).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Регрессия от BUG-TORCH-CHOMP-1 (пламя перевели на запекание в фон).
Аргумент «запекать безопасно, кадры пламени самонакрываются» верен для
пикселей самого факела, но не для чужой графики в той же ячейке: пламя
рисуется в клетке ПРАВОГО СОСЕДА (seg008:560), и челюсти чомпера возвращал
поверх огня только его собственный trob — пока анимация жива.
SDLPoP так не делает: animate_torch (seg007:0241) заканчивается вызовом
set_redraw_anim_right(), который метит правого соседа, а redraw_needed
(seg008:0178) рисует его слой строго в порядке draw_tile_anim_topright ->
draw_tile_anim_right (пламя) -> draw_tile_anim (СВОЯ графика тайла). То
есть челюсти возвращаются поверх огня КАЖДЫЙ кадр факела, независимо от
собственной анимации чомпера. У нас пламя рисуется напрямую, минуя
механизм пометок, — этой второй половины не было.
Фикс: после pop_torch_draw метим правого соседа POP_RD_CHOMP на ОДНУ
страницу (факел анимируется каждый кадр -> обе страницы получат свою
перерисовку по очереди). Порядок сходится сам: блок факелов идёт до
pop_redraw_needed. Код соседа читается в том же префетче кодов тайлов
(trob_rcode[]), чтобы не свапать W0 второй раз за кадр. Банк 6 +104 Б.
Осознанное расхождение (оригинал метит соседа безусловно, мы — только под
чомпера) заведено открытым: TORCH-ANIM-RIGHT в bug_list.md. Слой
draw_tile_anim рисует ещё пики/зелье/меч, но такого соседства на уровнях
1-4 не встретилось, а безусловная пометка стоит перерисовки тайла каждый
кадр на каждый факел.
tests-host: все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обе сборки прогнаны полным циклом (make hdd -> рестарт MAME -> уровень 1),
сцена «комната 1, Кид стоит, соперника нет», скриншоты идентичны. Фазы
сняты брейкпоинтами на out (_io_border), a, медиана по 60 кадрам:
спрайты 129 568 -> 127 510 (-2 058)
работа/кадр 391 258 -> 389 221 (-2 037)
остальные фазы совпали такт в такт
Счётчик делений (bp на __divsint/__modsint/__divuchar/__moduchar с
печатью адреса возврата):
комната 1, только Кид : 1,00 __divsint/кадр (возврат 0xD3AA = pop_cdraw) -> 0
комната 3, бой стража : 2,01 __divsint/кадр -> 0
Экономия ровно в фазе спрайтов и ровно на стоимость одного вызова
(2 058 тактов против документированной оценки ~2 400).
Честные оговорки записаны в TASKS_OPEN: период цикла как был 3 растровых
кадра, так и остался (выигрыш ушёл в запас, 40 800 вместо 38 700);
остальные правки в этой сцене не срабатывают; тайминги комнаты 3
несравнимы между прогонами (живой страж + pop_char_skip_mask), оттуда взят
только счётчик делений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Гипотеза «двухбайтная индексация съест выигрыш от сложения» не
подтвердилась. Собраны ОБА варианта, такты посчитаны по сгенерированному
asm (хвост после проверки границ):
int16_t готовое: add hl,hl / add hl,de / ld e,(hl) / inc hl / ld d,(hl)
= 132 такта, 2 304 байта
int8_t x/7: add hl,bc / ld a,(hl) / ld l,a / rlca / sbc a,a /
ld h,a / add hl,de = 131 такт, 1 152 байта
Расширение знака плюс 16-битное сложение стоят ровно столько же, сколько
лишний add hl,hl при двухбайтном индексе, а обращений к памяти на одно
меньше — под wait-state'ами Sprinter (2,4x номинала именно на обращениях
к ОЗУ) байтовый вариант ещё чуть выгоднее номинала.
Банк 4: 9 394 -> 8 243 из 16 384 (свободно 8 141 вместо 6 990) — запас под
рост pop_cdraw, о котором и был вопрос.
Тест переименован в geom_mul8div7_table_rules и проверяет ОБА правила
генерации таблицы: тождество 8x/7 == x + x/7 и усечение к нулю.
tests-host: [geom] 1992 -> 3144, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первая версия крыла только 0..255 байтовой таблицей x/7 по тождеству
8x/7 == x + x/7. Это было мимо: obj_x = 2*fwd - 116 уходит в минус, как
только fwd < 58 (левее x_bump[5]) — то есть у ЛЕВОЙ КРОМКИ комнаты, и там
мы продолжали звать __divsint.
Границы взяты из данных, а не на глаз: kid_data.bin даёт dx кадров Кида
-5..+10, стража -2..+10; при Char.x типа uint8_t и render_dx из
{-140,0,+140} полный диапазон obj_x = -416..695. SCRX[1152] кроет
-448..703 — деление стало недостижимым, оставлено страховкой.
Хранится ГОТОВОЕ значение (int16_t), а не x/7: байтовая таблица вдвое
меньше, но со знаковыми значениями требует расширения знака плюс
16-битного сложения — те же такты, что лишний add hl,hl при 2-байтном
индексе. Кодоген проверен: индекс полный 16-битный (грабли
sdcc_z80_const_ptr_index_bug обойдены отдельной uint16_t-переменной),
~130 тактов номинала против ~1 000 у __divsint.
Банк 4: 7 336 -> 9 394 из 16 384 (свободно 6 990). Таблица сверена
питоном обратно из .c (1 152 записи), правило генерации «усечение к нулю»
закреплено тестом geom_mul8div7_trunc_to_zero на целевом компиляторе.
tests-host: [geom] 1961 -> 1992, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обход всех сгенерированных .asm (awk по `call __div/__mod/__mul` с
привязкой к строке исходника) нашёл 31 вызов в 9 модулях. Три из них
были в горячем пути:
- pop_cdraw.c calc_screen_x_coord: `x * 8 / 7` -> __divsint, 2 400 тактов
на ПЕРСОНАЖА КАЖДЫЙ КАДР (два вызова при живом сопернике). Заменено
тождеством 8x/7 == x + x/7 плюс таблица DIV7[256] в банке 4 — резидент
не тронут, обычный диапазон (obj_x 0..252 при x_bump 58..184) покрыт
целиком, деление осталось только хвостом для шва (render_dx = ∓140).
- pop_guard.c guard_col_from_x: /14 и %14 звались БЕЗУСЛОВНО, мимо
POP_TILE_DIV — единственное 16-битное деление без подключённой таблицы.
- pop_trob.c animate_chomper: `tp / 10` на чомпера каждый кадр, при том
что TP_ROW/TP_COL лежали в этом же файле, но ниже по тексту. Таблицы
подняты выше чомперов.
Остальные 25 оставлены осознанно и расписаны в TASKS_OPEN.md: хвосты за
таблицей (x вне 0..255 = персонаж в соседней комнате), намеренный
медленный хвост pop_y_to_row, недостижимая ветка pop_rnd_fit и холодные
места (вход стража, старт уровня, читы, имя файла, отладочный HUD).
В банках 4, 6, 7 теперь ноль __div*. Тождество 8x/7 закреплено тестом
geom_mul8div7_identity (перебор −420..700), таблица DIV7 сверена с x//7.
tests-host: [geom] 840 -> 1961, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mob_tick_one (927) и mob_render (976/977) считали `(y+60)/63 % 4 - 1`
вручную, хотя pop_y_to_row — точный эквивалент этой формулы на всём
int16_t (включая усечение деления к нулю для отрицательных). В asm это
были три пары __divsint+__modsint, ~16 200 тактов (3,8 % кадра) — только
пока кусок плиты в полёте, то есть в самом тяжёлом кадре.
В банке 7 теперь ноль __divsint. Эквивалентность закреплена тестом
geom_y_to_row_matches_formula: перебор −400..400 против исходной формулы
(вызовы разбросаны по трём банкам, соблазн написать деление «по месту»
возвращается). tests-host: [geom] 39 -> 840, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tile_at_kid (guards.c) считала колонку честным / и %, хотя резидентная
POP_TILE_DIV — это и есть tile_div_tbl оригинала, и остальной порт давно на
неё переведён. У SDCC z80 пара / и % над int это __divsint плюс __modsint,
который внутри снова зовёт __divsint — ~5 400 тактов на вызов.
Зовут её в ЦИКЛЕ по колонкам между стражем и Кидом
(check_can_guard_see_kid, seg003:761). Когда Кид у шва, его curr_col = −1,
луч тянется через всю комнату: замер дал ВОСЕМЬ пар делений за кадр,
около 43 000 тактов = 10 % растрового кадра, в фазе логики. После фикса
таких вызовов не остаётся.
Как ловилось: брейкпоинт на __divsint с печатью адреса возврата дал
ret=C033 восемь раз за кадр; остановка на нём с dasm при замапленном банке
показала HL−65 / ld de,#14 / call __divsint по смещению 0x24 банка 1.
Снята и ложная тревога из прошлого коммита: пролог pop_char_fore на шве НЕ
разбухает до 134 730 — это была ошибка зонда (адрес fore_tile в банке
совпадает с кодом других банков, в интервал попадали чужие срабатывания).
Чистый замер: пролог 16 950, как и в середине комнаты. Урок записан в
«Как мерить» в docs/perf_backlog.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три шага пункта 0 из docs/perf_backlog.md.
1. Ранний выход из wall_pattern_palace по окну fore-клипа: узор целиком
лежит в x [xh*8, xh*8+32), y [dmy-59, dby], и если окно его не задевает
— возврат до первой заливки. Замер: от входа в узор до конца всего
прохода 6 055 тактов вместо ~60 000 на тайл.
2. Отсев КАЖДОГО декаля (wp_blit) по реальному габариту. pop_blit_b тоже
отсеивает до маппинга страницы, но по заведомо большему 64x64 — куски
кладки высотой 3..12 px он пропускал и платил полный
atlas_image + gfx_w0_map (~9 760 тактов на блит), чтобы там обнаружить,
что рисовать нечего. Размеры сняты из каталогов атласов и заданы верхней
границей по группе.
3. То же для ПОДЗЕМЕЛЬЯ: ранний выход wall_pattern (габарит выше — левая
марка уходит на dby+POP_YOFF-67) плюс wp_blit на RNDBLOCK, обоих
разделителях и обеих марках.
Замер (уровень 4, комната 18, Кид сдвигается читом ] по пикселю, skip
выключен, шесть тайлов в fore-окне, три из них — дворцовая стена):
тайл стены ~12 700 вместо 60 000-74 000, fore-проход целиком 70 681 вместо
171 693, работа за кадр 419 839, период 3 растровых кадра вместо 4.
Уровень 1 (подземелье) проверен снимком — кладка, марки, разделители на
месте.
Остаток в проходе — пролог pop_char_fore 17 208 тактов (пункт 1 backlog'а).
Отдельно записано: на ШВЕ пролог разбухает до 134 730, причина не разобрана.
BUG-CHEAT-FIGHT-1 (заведён 2026-08-07) закрыт по своему же плану: ветка
ROOMNAV после kid_init/pop_kid_hp_reset гасит состояние схватки
(Kid.sword = SWORD_0_SHEATHED, holding_sword, offguard, guard_refrac).
Меч в инвентаре не теряется. Это болезнь чита: в оригинале телепорта между
комнатами нет и в режим боя без соперника попасть нечем.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ОПТИМИЗАЦИЯ. Метка «фон трогали» была одним union-прямоугольником на
страницу, и три факела комнаты склеивались в полосу x 40..280 на всю
комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки
и каждый кадр платил полным fore-проходом. Теперь это маска тайлов
(uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница),
проверка — три AND через резидентный pop_cd_hit. Гранулярность тайла — это
гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe;
подтайловое уточнение там только по высоте, wipe_heights). Замер на (1,7):
циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3
растровых кадра.
BUG-LOOSE-BUTTON-1. Упавшая плита не нажимала кнопку. Три слоя: порт
loose_land не звал trigger_button вовсе; pop_room_col_landing считала
площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки,
зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего
ворота начинали открываться, пока она ещё в воздухе. Нажатие в оригинале
ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ»
(modifier 0xFF), и кнопка съедается. Посадка в комнате снизу переехала на
новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже
поля).
BUG-GATE-FF-1. 0xFF был перегружен: сторожевое «тайла нет» в gate_modif и
живое «открыто навсегда» из trigger_gate. gate_passable заворачивал Кида в
воротах, нарисованных открытыми. Мёртвая ветка убрана.
BUG-TORCH-CHOMP-1. Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и
стирал пламя факела (0,6), которое рисуется в ячейке правого соседа. Фикс —
запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем
канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера
сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале.
Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE.
Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл
tiles_5_stuck в атласе.
Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1)
-> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host —
все 5 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BUG-GUARD-IX-1 — «зависание» в бою со стражем. Не зависание: главный цикл
крутился, а отладочная локаль frozen сама становилась ненулевой. Корень —
у main затирался IX (0xBFFA -> 0xBF00), и все его локали адресовали живой
стековый мусор. Затирал check_chomped_guard: coll_row() пишет по
flags + scan_off, длину берёт из win_lo/win_hi, а scan_off выставляла
только coll_scan_prepare() из пути Кида. Ряд стража писался по смещению
Кида длиной стража и при Киде у правого края комнаты вылезал за flags[13]
— прямо в сохранённый IX. Фикс: coll_scan_prepare() в начале
get_row_collision_data(); заодно чинится расчёт (scan_left0 задаёт x
колонок, чомпер-коллизия стража считалась по координатам Кида).
На уровне 1 не проявлялось: бой идёт левее середины, запись оставалась
внутри массива — данные были неверны молча.
BUG-GATE-SEAM-ROW1 — решётка в шве не анимировалась. Плита комнаты 1
открывает ворота комнаты 8 в (1,9), видимые через левый шов, а
change-driven редрой смотрел только m[9] (ряд 0) и перерисовывал жёстко
draw_tile(0,0). На уровне 1 та же связка работала лишь потому, что
решётка соседа стояла в (0,9). Фикс: сигнатура по всем трём рядам,
маска изменившихся рядов в seam_rows (переживает оба кадра дабл-буфера),
pop_room_redraw_seam_left(rows) перерисовывает только помеченные. Плюс
ряд выше (changed | changed>>1): верх решётки (draw_tile_anim_topright,
seg008:0568) рисует тайл над-справа от ворот, без этого чёрный
треугольник над ними оставался статичным.
Проверено пользователем в MAME (бой в комнате 18; анимация решётки в
стартовой комнате), детекторы IX висели без починки и не сработали;
make -C tests-host — все 5 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разбивка логического кадра брейкпоинтами (уровень 4, Кид неподвижно на
(1,7)): работа 517 609 тактов = 120% растрового кадра, период 4 кадра.
Циан (PROF(6)) — 233 515 = 54% растрового кадра, из них
pop_char_fore(KID) = 171 693.
Внутри fore-прохода шесть тайлов, и два из них — СТЕНА ряда 2 под ногами
Кида — стоят 74 310 и 65 553. Дворцовая кладка на тайл: 6 wpp_fill
(~3 100) + 5 pop_wall_b (~9 760) ≈ 60 000. Окно клипа в этот момент
x 229..241, y 106..147 (прочитано из pop_t_fclip_*), верхний кусок узора
стоит на y=157 — не пересекается вовсе, тайл (2,6) промахивается и по x.
То есть 139 863 такта за кадр рисуют ноль пикселей; снятие уводит период
с 4 растровых кадров на 3.
Записано пунктом 0 в docs/perf_backlog.md с тремя вариантами лечения.
Состав узора сверен с SDLPoP (seg008.c:1943) — порт дословный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом (наблюдение пользователя): кирпичи дворцовой кладки правильного
цвета, а швы между ними — нет.
Корень шире, чем швы. init_game_main (seg000:164) подменяет ДВЕ записи
16-цветной палитры сразу после загрузки:
// (blood, hurt flash) #E00030 = red
set_pal(12, 0x38, 0x00, 0x0C);
// (palace wall pattern) #C09850 = light brown
set_pal( 6, 0x30, 0x26, 0x14);
Подтверждено дампом палитры живого SDLPoP: PAL[6] = 48,38,20,
PAL[12] = 56,0,12. У нас в таблице VGA16 стояли стандартные VGA-цвета
(42,21,0) и (63,21,21). Цветом 6 рисуются швы дворцовой кладки
(blitters_46h_mono_6), цветом 12 — кровь чомпера и вспышка урона, так что
промах был не только в стенах.
Заодно исправлена вспышка урона в roomtest.c: было flash_bg(255,85,85)
(стандартный brightred), стало (224,0,48).
Проверка: ряд стен уровня 4 теперь совпадает с эталоном SDLPoP по всем
восьми цветам и их количествам один в один (5333/4013/3110/2065/1785/1372/
1075/447). Остаточное различие значений — только наше масштабирование
6->8 бит: (v*255)/63 против v*4 у SDLPoP, то есть (194,153,80) против
(192,152,80).
ПОПРАВКА к f107711: там записано, будто рисунок кладки не совпадает с
оригиналом из-за замены PRNG. Это неверно — POP_PRANDOM_EXACT по умолчанию
1, работает ассемблерный LCG оригинала, и совпадение счётчиков цветов это
подтверждает. Расхождения по PRNG нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Во дворце тело стены — не кирпичи-спрайты, а шесть СПЛОШНЫХ ЗАЛИВОК плюс
пять моно-разделителей поверх (seg008:1946). Порт целиком:
- gen_palace_wall_colors (seg000:1942): 3 ряда × 4 подряда × 11 колонок = 132
цвета, сид = номер комнаты, подряды 1/3 из 0x61..0x64, подряды 0/2 из
0x66..0x69, соседние по горизонтали не повторяются. Одиннадцать колонок,
а не десять: заливки 3 и 5 берут цвет СЛЕДУЮЩЕЙ колонки. Пересчёт на
смене комнаты — там же, где сбрасывается кэш кладки (wall_pattern_reset).
Таблица не static: writable-данные банка живут в _DATA/W2.
- Геометрия заливок дословно из add_wipetable(layer, left, bottom, height,
width): прямоугольник = x..x+width-1, (bottom-height+1)..bottom.
- Пять prandom(2) на тайл кэшируются так же, как подземельные решения
(wp_a/wp_b переиспользуются — наборы в одной комнате не сосуществуют).
Сохранён квирк порядка: при which_part == 0 разыгрывается ОДНО значение, и
нижний разделитель берёт ПЕРВОЕ из серии, а не пятое.
- Заливки режутся по окну fore-клипа: иначе легли бы поверх областей, которые
в этом кадре никто не восстанавливает. Вне fore-прохода — pop_cd_touch,
потому что bar идёт мимо pop_blit_b.
- wall_fram_bottom / wall_fram_main во дворце НЕ рисуются (seg008:576, 711) —
и в горячей половине слоя (pop_bg.c), и в холодной (pop_room.c).
- Упаковщик: дворцовые wall-id 3..17 пакуются силуэтом в цвете 6 общей
16-цветной палитры (blitters_46h_mono_6). В подземелье те же id —
обычные кирпичи, поэтому mono только у паласного набора.
ИЗВЕСТНОЕ РАСХОЖДЕНИЕ: рисунок цветов не совпадает с SDLPoP попиксельно,
потому что наш prandom — 16-битный xorshift, а не LCG оригинала (замена
сделана раньше по бюджету кадра, prng_alternatives.md). Совпадают
геометрия, диапазоны цветов и правило «соседние не повторяются».
Проверено в MAME: уровень 4 — песочный мрамор с разделителями, структурно
как эталон SDLPoP; уровень 1 не изменился. tests-host 5/5.
Остаётся расхождение по двери уровня (мы заполняем проём плетёнкой целиком,
оригинал рисует несколько кусков лестницы) — отдельным шагом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт семи мест seg008, расходящихся по tbl_level_type, плюс общая дыра
порта, которая на дворце стала видна.
Паласные ветки (все — «в подземелье этого нет»):
- doortop_fram_top / doortop_fram_bot (seg008:413, 506): декоративная панель
над воротами. У шва она и есть тот «ковёр», которого не хватало.
- stripe_id соседа слева (seg008:486): орнаментная лента под окнами. Она
непрерывная, потому что stripe_id = 145 у пола, кнопок, зелья, loose,
чомпера и меча; без неё лента шла кусками (только blueline).
- полоска на стене id 84 (seg008:510), при (modifier & 0x80) == 0.
- blueline_fram3: условие `num == !!level_type` — в подземелье пропускается
num==0, в паласе num==1 (seg008:501).
- левая половина кнопки-opener без пола (id 148) — только подземелье
(seg008:628).
- склянка зелья: id += 2 во дворце (seg008:747).
- remove_loose возвращает ТИП УРОВНЯ, и он ложится модификатором пустой
клетки от упавшей плиты (seg007:846/1083).
Потерянный вывод (НЕ паласное расхождение, просто заметили здесь):
draw_tile_anim_topright (seg008:0568) не был портирован вовсе — верх ворот,
который рисует тайл НАД ними: маска 68 (mono, чёрным) + door_fram_top
[(modifier>>2) % 8] = 60..67. Ids 60..68 в атлас не паковались. Симптом —
чёрный клин над воротами; нашёлся сравнением с эталоном SDLPoP
(--screenshot) и трассой add_backtable.
Флаг тайлсета pop_palace вынесен в резидент (pop_tile.c): по нему расходятся
ветки в банке 7 (полная отрисовка), банке 2 (fore-проход) и банке 3
(модификатор пустой клетки) — читается напрямую, без трамплина.
Известное расхождение: модификатора ряда СНИЗУ у нас нет (pop_t_below —
только fg), поэтому паласная панель над воротами в комнате снизу не
рисуется. Помечено в коде.
tests-host: 5/5 (добавлен include-путь до pop_bg_atlas.h и стаб pop_palace).
Проверено в MAME: уровень 4 совпадает с эталоном SDLPoP в рядах 0-1
попиксельно (кроме фазы пламени); уровень 1 не изменился.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом (эталон SDLPoP против нашего кадра, уровень 4): дворцовая геометрия
рисовалась подземельными красками — сине-серые арки вместо песочных,
бирюзовая дверь уровня вместо кремовой, сланцевый пол вместо
коричнево-розового. Бирюза и зелень — это dungeon-слоты 0x5E (0,117,76) и
0x5F (0,165,157).
Причина в порядке старта: атласы (и вместе с ними палитра тайлсета) грузятся
ДО initgraph, потому что тот снимает DSS-страницу W0. А kid.pal — ЕДИНАЯ
игровая палитра, собранная из VDUNGEON (pop_pack_kid.py build_palette), —
читается ПОСЛЕ initgraph и затирает слоты 0x50..0x6F.
pop_bg_pal_apply() возвращает 32 записи текущего набора; зовётся сразу за
gfx_pal_sync(). На смене уровня палитра по-прежнему едет внутри
pop_bg_load — там initgraph давно позади.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один и
тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE.
Упаковщик (toolchain/pop_pack_bg.py):
- аргумент набора: `pop_pack_bg.py dungeon|palace`. Каскад каталогов —
сначала свой набор, потом чужой фолбэком (в распакованном data/ res230/
231/348 есть только в VDUNGEON, два десятка — только в VPALACE).
- ОБА набора пакуются по одному объединению id, поэтому раскладка
id -> (страница, idx) общая и заголовок один: коду достаточно подменить
имена файлов.
- PALACE_ENV_IDS: 78/80/82 (doortop_fram_bot), 81/83 (doortop_fram_top),
84 (полоска стены), 145 (stripe_id) — их рисует только палас, render_room
про них не знает.
- ENV_SHIFT 5 -> 4: с паласными кусками страница 2 переваливала за 16 КБ
(16 996). Цена — 10 страниц EMM на набор вместо 5, при 215 свободных.
- *tile.pal: 32 записи (env 0x50..0x5F + wall 0x60..0x6F) на набор. Полная
kid.pal не трогается — Кид, страж, меч и зелья в других слотах.
Движок:
- pop_level_type() (tbl_level_type, SDLPoP data.h:840): дворцовые уровни
4, 5, 6, 10, 11, 14. Живёт в pop_level.c, потому что по типу расходятся
не только атласы, но и ветки отрисовки seg008, кладка стены и модификатор
пустой клетки от упавшей плиты (remove_loose, seg007:0EB8).
- pop_bg_load(set): no-op при том же наборе, при смене выгружает старый
(иначе текут 12 EMM-страниц) и правит 32 записи палитры в ОБЕ страницы
дабл-буфера. Зовётся на старте и на границе уровня, не в кадре.
- Путь к атласу склеивается на месте (bg_path): двадцать строк-имён в банке
— лишние полкилобайта.
Проверено в MAME: уровень 1 (подземелье) рисуется как прежде; уровень 4
(-DFIRST_LEVEL=4) — дворцовые арки, окна, пол, решётчатая дверь уровня,
Кид не перекрашен. Стены пока чёрные: паласный wall_pattern — шаг C.
tests-host: 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Клипованный путь вынесен в отдельную функцию blit_b_clip: под его девять
16-битных локалей SDCC заводит кадр IX, и за этот кадр платили ВСЕ блиты
фона, включая те, где клипа нет вовсе (весь фон вне fore-прохода — факелы,
зелья, перерисовка тайлов, у них pop_t_fclip_on == 0). Быстрый путь идёт
сразу в gfx_blit_noclip.
Замер: pop_torch_draw 41 778 -> 31 218 тактов на факел (часть разницы —
прошлая правка pop_cd_touch; чистый вклад этой ~6 300 на блит). Поведение
не изменилось: клипованная ветка перенесена дословно.
docs/perf_backlog.md — отложенные идеи с измеренной ценой (футпринт из
физики 11 574, размеры ленты из каталога атласа, один map/unmap на группу
блитов, единый проход по тайлам как redraw_needed_tiles, objtable,
отложенные таблицы back/mid/fore) плюс раздел «как мерить»: wait-state'ы
дают 2,4x к справочным тактам, кадр 430 000, адреса символов меняются
после каждой пересборки, сцена между сессиями не воспроизводится.
Там же — что уже проверено и НЕ сработало.
tests-host: 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сближение с SDLPoP, замером НЕ подтвердилось — фиксирую как есть.
- FORE_ANY[32]: есть ли у кода тайла хоть что-то в переднем слое. Порт
ранней проверки оригинала `if (tile_table[curr_tile].fore_id == 0) return;`
из начала ветки default в draw_tile_fore (seg008:0D15), расширенной нашими
спецслучаями (стена 20, пики 2, чомпер 0x12, нижняя грань loose 11 — они
рисуются мимо fore_id). Была в самом конце цепочки сравнений.
- ft_code/ft_x/ft_dmy: контекст тайла считается один раз на тайл, как глобалы
curr_tile/draw_xh/draw_main_y у load_curr_and_left_tile. Код тайла читался
дважды (fore_tile и снова fore_only_tile), координаты — в каждом листе.
Замер (Кид на 2,4 в щебне, MAME): pop_fore_over_char 58 764 -> 58 866, то
есть в пределах шума. Ранний выход не срабатывает — в футпринте Кида тайлы
почти всегда С передним слоем; снятое второе чтение кода съедено проверкой
FORE_ANY и записью контекста. Отрисовка не изменилась: щебёнка блитится теми
же параметрами (x=150 y=208 sx=22 w=10 h=2).
Где время на самом деле: ~16 000 тактов фиксированных накладных на КАЖДЫЙ
блит фона независимо от размера (atlas_image + gfx_w0_map + чтение w/h через
окно 0 + арифметика клипа + unmap). Блит щебёнки 10×2 обходится в 21 168,
факел 16×22 — в 31 000 при самом gfx_blit_noclip 8 706.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Главный цикл спейсится тремя gfx_wait_vsync, поэтому работа сверх 430 000
тактов стоит сразу целый лишний растровый кадр. Было 470 964, стало
~425 600 — игра быстрее на треть (16,7 логических кадров/с против 12,5).
- pop_y_to_row: цепочка сравнений вместо (y+60)/63%4-1. ВАЖНО: медленный
хвост вынесен в ОТДЕЛЬНУЮ функцию — SDCC видит одинаковое выражение в двух
ветках и поднимает деление в вершину, быстрые возвраты не спасают.
- col_from_x (pop_bg) и get_tile_div_mod (pop_map) — общие резидентные
таблицы POP_TILE_DIV/POP_TILE_MOD в pop_tile.c (const банка из чужого
банка не читается).
- pop_fore_over_char: расширение окна считается арифметикой, а не перебором
10 колонок и 3 рядов (условие монотонно -> границы). Формулы сверены с
прежним перебором перебором значений, расхождений нет.
- pop_cd_touch: цикл по страницам развёрнут, x+w/y+h считаются один раз.
Зовётся с каждого блита фона, стоил 6 846 тактов.
- process_trobs: tp/10 и tp%10 у факелов — таблицей.
- Пустой слот соперника (стража на сцене нет, на странице ничего не
нарисовано) считается «тихим»: ни heal, ни вход в pop_char_draw, ни
fore-проход.
Приём для поиска делений: брейкпоинт на __divsint/__divuint/__divuchar с
печатью адреса возврата (printf "%04X", w@(sp)).
Профиль остатка — TASKS_OPEN.md#draw-cost. tests-host: 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- move_coll_to_prev -> memcpy (LDIR): цикл на C пересчитывал адрес
назначения через слот кадра IX и обходился в 5 514 тактов на 14 байт.
- Окно перебора режется на НЕПРЕРЫВНЫЕ пробеги (комната слева / своя /
справа), по каждому идёт coll_scan с шагающим указателем. Прежний
«быстрый путь для окна внутри комнаты» не срабатывал почти никогда: Кид
в колонке 0 даёт окно с −1, и всегда шёл медленный сбор во временный
буфер с тернарником на колонку (1 340 тактов на колонку).
- Пролог ряда (координата грани, длина окна, смещение слота) вынесен из
тела ряда на кадр; базы соседних рядов — те же ±10 без пересчёта.
check_collisions 44 022 -> 38 334, физика Кида 67 518 -> 63 102, работа за
логический кадр 470 964 -> 466 560 (бюджет растрового кадра 430 000).
Замер итерации: пустая колонка 750 тактов, колонка-стена ~1 700.
tests-host: все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Замерено брейкпоинтами в MAME (уровень 1 комната 1, Кид стоит у факела).
Калибровка, без которой цифры не сходятся: такт totalcycles != номинальный
T-такт Z80, wait-state'ы ОЗУ Sprinter дают ~2,4x (get_tile 574 против 1422).
1. Окно перебора коллизии — как у оригинала (left_checked_col..right_checked_col,
seg004:0047), было: все 14 колонок каждый кадр. Признак годности слота у
нас дешевле оригинального: не массив номеров комнат с очисткой, а границы
окна, которые move_coll_to_prev переносит в prev вместе с флагами; бамп
считается по пересечению двух окон. check_chomped_flags тоже ограничен
окном, иначе протухшие слоты дают фантомный перемол.
2. get_tile_div_mod — таблицами tile_div_tbl/tile_mod_tbl (seg006:702), было
/14 и %14. SDCC разворачивал это в __divsint + __modsint, а __modsint
внутри зовёт __divsint ещё раз: 5 400 тактов на вызов, 13 вызовов за
кадр = 16 % кадрового периода на «в какой колонке точка».
3. get_row_collision_data: ряд разрешается один раз на весь перебор (было —
get_tile на каждую колонку, 1 422 такта), грань идёт шагом TILE_SIZEX как
в оригинале, wall_type таблицей вместо switch.
Итог: check_collisions 60 888 -> 42 750, физика Кида 100 578 -> 67 518,
синяя полоса ~60 % -> ~30 % кадрового периода. Профиль остатка и следующие
цели (отрисовка Кида 47 %, process_trobs 21 %) — в TASKS_OPEN.md#draw-cost.
tests-host: все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Оригинал (seg004:0004) индексирует флаги перекрытия колонкой ВНУТРИ
разрешённой комнаты и хранит рядом её номер, поэтому решётка комнаты 8
остаётся в своём слоте и после перехода 8->6: переход флага 0->1 виден,
bumped() срабатывает. У нас индекс — колонка отрисованной комнаты, тот же
тайл менял слот, и enter_room вынужден был выбрасывать историю целиком —
на кадре входа бампа не было, и Кид с разбега уходил сквозь закрытые ворота.
Вариант B (сдвиг вместо тега комнаты): при БОКОВОМ переходе история не
выбрасывается, а перенумеровывается на 10 слотов. check_leave двигает x
ровно на ∓140 = 10 тайлов, координата грани едет на те же 140 вместе с
габаритом Кида — сами флаги инвариантны, меняется только номер слота.
Сдвигаются curr/above/below (prev на следующем кадре всё равно перезапишет
move_coll_to_prev), освободившиеся слоты = 3 «уже перекрывал».
Вверх/вниз и прочие входы в комнату — по-прежнему полная инвалидация.
Дословный вариант A (10 слотов + массив номеров комнат) не взят: он тянет
за собой сужение окна перебора колонок, то есть отказ от FIX_COLL_FLAGS.
Заведён docs/impl_diff.md — список осознанных расхождений с SDLPoP; правило
«фиксировать расхождение» в обоих CLAUDE.md теперь указывает туда.
tests-host: все 5 наборов прошли. Приёмка в MAME впереди.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сценарий: ур.1 комн.8, ряд 0, x=170, лицом вправо, решётка шва закрыта —
разбежаться вправо и не отпускать. Кид проходит сквозь решётку.
Корень (две трассы, наша и оригинала, стартовая позиция совпала до пикселя):
смена комнаты в этой точке ШТАТНАЯ и у нас, и в оригинале — leave_room вправо
блокируют только doortop. Оригинал держит Кида бампом СРАЗУ ПОСЛЕ перехода,
потому что check_collisions хранит флаги по паре (колонка в СВОЕЙ комнате,
номер комнаты) и сравнивает prev/curr только при совпадении номеров: решётка
комнаты 8 и до, и после перехода лежит в слоте 9 с room=8, история не рвётся.
У нас индекс — колонка относительно отрисованной комнаты, при переходе все
индексы уезжают на 10, поэтому enter_room зовёт pop_coll_invalidate, тот
подавляет бамп на кадре входа, а дальше перехода флага 0->1 уже не будет.
Что делать — порт индексации оригинала (10 слотов по колонке своей комнаты +
массив её номера); тогда pop_coll_invalidate не нужен вовсе. Осторожно: это
сердце коллизии из BUG-SEAM-PINGPONG.
SDLPoP инструментирован для сверки: POP_TRACE=1 включает покадровую печать
Char + обе пары краёв, плюс маркеры BUMPED и LEAVE. В bug_list записана и
команда сборки на macOS (штатный Makefile требует pkg-config, которого нет).
073a6e0 вернул в safe_step(distance == 0) оригинальный seq_39 — а это шаг на
ОДИННАДЦАТЬ пикселей, который останавливает только бамп. У ворот шва
(ур. 1, комн. 6) наш бамп срабатывает не на том же кадре, что у оригинала
(своя история — BUG-SEAM-PINGPONG), и Кид с разбега ПРОБЕГАЛ сквозь
закрытую решётку между комнатами 8 и 6. Мелким шагом упирался нормально —
то есть промах именно в длине шага, а не в самом бампе.
Теперь ветка разделена по препятствию:
ЧОМПЕР — оригинальный seq_39: бампа там нет по определению (is_obstacle
требует modif == 2), и шаг обязан пройти целиком, иначе Кид топчется на
1 px и не успевает между челюстями;
СТЕНА И ВОРОТА — прежний «шаг-1»: осознанное приближение, проверенное
трассой живого SDLPoP в BUG-SEAM-PINGPONG. Останется таким, пока наш
бамп не сойдётся с оригиналом покадрово.