# План: порт `clip_char()` — обрезка спрайта персонажа Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8» (Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке). ## Симптом room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке. ## Корень Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из `add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ `set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД `add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только внутри него. Мы рисуем без клипа вообще. ## Что делает оригинал (дословно) `reset_obj_clip()` → `left=0, top=0, right=320, bottom=192`. Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый `get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена ИЛИ пол»): ``` T_L = get_tile(room, char_col_left, char_top_row) T_R = get_tile(room, char_col_right, char_top_row) if (T_L — стена или пол) && ( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */ || (T_R — стена или пол) ) { clip_row = Char.curr_row + 1; clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */ if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y)) obj_clip_top = char_top_y = clip_y; } ``` Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»). Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО. Метрики из `set_char_collision` (seg006:0723): - `char_x_left = obj_x/2 + 58` (и `-= char_width_half`, если смотрит вправо) - `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2` - `char_top_y = obj_y - h + 1`; если `>= 192` → `0` - `char_top_row = y_to_row_mod4(char_top_y)` - `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`, `char_col_right = MIN(get_tile_div_mod(char_x_right), 9)` Второй блок `clip_char` — `obj_clip_right` (doortop / стена / зеркало для виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228 (выход в дверь уровня). **Фаза 2**, не в этом заходе. ## Наши точки касания - `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`. - `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`, `get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже есть в `check_spike_below()` (строка ~1198) — брать за образец. - `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0` пропускает `sy` верхних строк колонки), но произвольной верхней границы нет. ## Шаги 1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`». Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник `+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`. Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро). 2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y (0 = клипа нет). Экспорт в `pop_map.h`. 3. **pop_kid.c**: в `kid_draw()` после расчёта `top` — `ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`, рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`; в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает `reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять). 4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`, `make size-check`; вывести свободное место в W2/W3 (порог 512 Б). 5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск. Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`). ## Риски / что проверить отдельно - `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт −1 для полосы у потолка; `get_tile(row −1)` уже умеет ряд 2 верхнего соседа. - Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип прыгнет. - `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой должно отсекать). - Клип меняет прямоугольник heal → возможен «хвост» на второй странице дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер). ## Дальше (Фаза 2, отдельно) `obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но без тестовой сцены проверять нечем. См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`, `pop_check_sdlpop_first`, `gfx_blit_noclip_fast`.