# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации

Только то, что **не закрыто**.  Всё закрытое (и, что важнее, разбор корней)
переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
грепни там по симптому.

Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь.  Правило проекта:
механику сверять с `../SDLPoP/src/` ДО кодинга.

Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1).  Одиннадцать
наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены
в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень
стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень),
BUG-DEATH-1 (смерть от меча), BUG-GATE-ANIM-1 (анимация ворот), BUG-COLL-1
(полный порт `check_collisions`), BUG-STANDUP-1 (лишний guard в
`bumped_floor`).  Клавиатурный BUG-KBD-3 доведён до конца во второй волне
(см. ниже); из первой волны открытым остался косметический BUG-SEAM-DRAW-1.

Ревизия **2026-08-03, вечер** (вторая волна с прогона): все четыре наблюдения
закрыты — BUG-KBD-4 (фантомный отжим Shift), BUG-RESPAWN-2 (стражи не оживают),
BUG-DRAWORDER-1 (Кид поверх тела стража) и BUG-LOOSE-2 (осколки только от одной
из двух плит).  У BUG-DRAWORDER-1 остался артефакт «ноги поверх головы»,
сверенный с SDLPoP и записанный в «НЕ БАГИ».  Тогда же закрыт как
невоспроизводящийся BUG-SEAM-DRAW-1 (Кид в шве рисуется) и заведён
BUG-GATE-PASS-1 — однократное наблюдение прохода сквозь закрывшуюся решётку,
которое ЖДЁТ надёжного сценария.

| ID | что | тип | статус |
|----|-----|-----|--------|
| [BUG-SPIKE-1](#bug-spike-1) | пробег по выдвинутым пикам не убивает: модификатор застревает в «полностью вышли» (h=1), а это в оригинале не смертельно для бегущего | Major | **открыт, замер есть, заведён 2026-08-05** |
| [BUG-GUARD-DEAF-1](#bug-guard-deaf-1) | страж не оборачивается на вернувшегося Кида: `is_guard_notice` не взводился | Major | **фикс есть, ждёт игровой проверки** |
| [BUG-GRAB-1](#bug-grab-1) | ур. 2 комн. 9: прыжок с места через 3 тайла — нет зацепа за кромку | Major | **причина найдена (клавиатура, BUG-KBD-5), фикс есть, ждёт игровой проверки** |
| [BUG-GATEMOD-1](#bug-gatemod-1) | ворота стартуют закрытыми, хотя в уровне открыты | Major | **фикс есть, ждёт проверки** |
| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **перепроверить после [BUG-GATEMOD-1](#bug-gatemod-1)** — та же решётка стартовала не в том состоянии |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |

---

<a id="уровень-2"></a>
# Уровень 2 — баги с приёмки (заведено 2026-08-04)

Уровень 2 заиграл 2026-08-04 (см. [`TASKS.md`](TASKS.md) запись **L2**).
Пользователь при первом прогоне увидел **несколько багов отрисовки** и
подаст их списком отдельно — раздел заведён под них заранее, чтобы
наблюдения не растеклись по переписке.

**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md#bug-loose-3) — чёрный
бар под упавшей плитой-потолком (запечка восстанавливала не всё, что стёрла).

**Порядок работы по уровню 2 (2026-08-05):** наблюдения СНАЧАЛА копятся
здесь целиком — фиксы берём, когда уровень пройден до конца.  Причина:
косметика отрисовки почти всегда сводится к общему корню (ср. волну
BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит
чинить один и тот же корень трижды.

Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие
кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass),
запись **L2-PASS**: по ней видно, «механика не сработала» это или
«так и задумано».

Перед заведением записи — грепнуть [`bug_closed.md`](bug_closed.md) по
симптому: почти вся косметика окклюзии уровня 1 уже разбиралась
(BUG-CEIL-1/2/3, BUG-OCCL-1, BUG-DOOR-CLIP, BUG-DRAWORDER-1), и уровень 2
вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9)
и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый.

<a id="уровень-3"></a>
# Уровень 3 — баги с приёмки (заведено 2026-08-05)

Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения.  Два
из них — **не баги, а неначатые задачи**, и записаны как таковые:

| наблюдение | что это на самом деле |
|------------|------------------------|
| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ.  В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе.  Так и должно выглядеть до порта |
| к.10: скелет не оживает | [L3-SKEL](TASKS.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован.  **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`.  Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда.  Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 |

Остальные два были настоящими багами и **закрыты в тот же день** — разбор
корней в [`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** (флаги
коллизий считались для одного ряда вместо трёх — бамп терялся на кадре
смены ряда) и **BUG-SEAM-WEDGE-1** (сосед угла (2,0) безусловно считался
стеной).

<a id="bug-spike-1"></a>
## BUG-SPIKE-1. Пики не убивают бегущего Кида — ОТКРЫТ, ЗАМЕР ЕСТЬ

**Наблюдение (пользователь, 2026-08-05).**  Уровень 2, комната 6, пики (1,3):

| действие | что происходит |
|----------|----------------|
| длинный прыжок с ряда 0 на пики | **смерть — правильно** (путь `fell_on_spikes`) |
| пробег по ряду 1 прямо по пикам | **урона нет** |
| после уборки пик | **на экране остаются белые остатки остриёв** (в оригинале чисто) |
| прыжок на месте, стоя на пиках | урона нет |
| просто стоять на выдвинутых пиках | можно сколько угодно |

**Замер (MAME, чтение `room_modif` комнаты 6).**  Пока Кид стоит на тайле,
модификатор пики (индекс 13) = **0x8E**, то есть «пики ПОЛНОСТЬЮ вышли и
идёт обратный отсчёт».  Дальше вся арифметика сходится с оригиналом:

```
is_spike_harmful (seg007:1178):  0/-1 → 0;  <0 → 1;  1..4 → 2;  >=5 → 0
check_spiked     (seg006:0658):  убивает при h>=2 на кадрах бега 7..14
                                 и при h!=0 на кадрах приземления 43/26
```

То есть **при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале** —
смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2).  Наши
`animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked`
сверены с seg006/seg007 построчно и совпадают дословно.

**Значит расходится ТАЙМИНГ, а не логика:** у нас пики успевают полностью
выйти ДО того, как Кид на них вбежит, и он проскакивает мимо смертельного
окна 1..4.  Триггер — `check_spike_below` (seg006:1199), и у нас он берёт
габарит спрайта из `kid_fp` — метрик ПОСЛЕДНЕГО отрисованного кадра, а
оригинал берёт `char_x_left/right` ТЕКУЩЕГО кадра (`set_char_collision`).
Это первый подозреваемый; второй — момент вызова `process_trobs`
относительно `play_frame` (сколько кадров модификатор успевает пройти,
пока Кид добегает).

**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом) — оно и уточняет
гипотезу.**  Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и
«спрятались не все — часть артефактов осталась».  Скриншоты рядом:
[наш](bugscreens/l2-r6-spikes-ours.png) и
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе.  У нас из-под
щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.

Это стыкуется с замером: пока Кид стоит рядом, `check_spike_below` каждый
кадр зовёт `start_anim_spike`, а тот при отрицательном модификаторе
переставляет его обратно в 0x8F — то есть пики у нас «залипают выдвинутыми»
и отсчёт до уборки не доходит.  В оригинале в той же позе они убраны,
значит его `check_spike_below` эту колонку УЖЕ не задевает.

**ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер
2026-08-05, MAME, watchpoint на `room_modif[13]` комнаты 6).**  Чистый
пробег по УБРАННЫМ пикам убивает штатно:

```
запись modif=1 : кадр 11 (беговой), x=112, curr_col=2   ← пики пошли вверх
запись modif=2 : кадр 12 (беговой), x=117, curr_col=3   ← Кид уже НА тайле, h=2
запись modif=3 : кадр 177 (frame_177_spiked)            ← напоролся
```

То есть `check_spike_below`, `check_spiked`, `is_spike_harmful` и тайминг
выдвижения работают правильно, и «раннего» триггера нет.

**Настоящий корень — пики ЗАЛИПАЮТ выдвинутыми.**  Пока габарит Кида
накрывает колонку пики, `check_spike_below` каждый кадр зовёт
`start_anim_spike`, а тот при отрицательном модификаторе переставляет его
обратно в 0x8F — отсчёт до уборки не доходит.  А выдвинутые пики (h = 1)
для бегущего БЕЗВРЕДНЫ по правилам оригинала.  Отсюда обе жалобы: пробег
по уже вышедшим пикам не убивает, и они же остаются торчать на экране.

**Что осталось выяснить (и это единственный открытый вопрос).**  Код
`start_anim_spike` у нас с оригиналом совпадает дословно, значит оригинал
тоже удерживал бы пики, стой Кид там же.  На скриншоте SDLPoP в похожей
позе пики УБРАНЫ — то есть его Кид стоит чуть левее и его габарит колонку
пики уже не задевает.  Разница в 2–3 пикселя посадки, а у нас такие
расхождения по X уже ловились (см. заметку в BUG-GRAB-1: после касания
площадки SDLPoP уводит Кида на 134, мы — на 141).

**Как закрывать:** инструментировать SDLPoP (печать `char_x_left/right`,
`left/right_checked_col` и модификатора пики каждый кадр), проиграть ту же
сцену — падение плиты-потолка в комнате 6 и остановку на щебне — и сверить
с нашей трассой ПОЗИЦИЮ КИДА после приземления.  Если позиции совпадут, а
диапазоны колонок разойдутся — виноват габарит (`kid_fp` против
`set_char_collision` текущего кадра); если разойдутся позиции — это отдельный
баг посадки, а пики — его следствие.

---

<a id="bug-guard-deaf-1"></a>
## BUG-GUARD-DEAF-1. Страж не оборачивается на вернувшегося Кида — ФИКС ЕСТЬ, ЖДЁТ ИГРОВОЙ ПРОВЕРКИ

**Наблюдение (пользователь, 2026-08-05).**  Уровень 2, комната 11, идёт бой
со стражем.  Страж **выталкивает Кида в правую комнату 22**.  Кид заходит
обратно в комнату 11 — страж стоит на (1,1), **повёрнут налево и Кида не
видит**.  Предположение пользователя: это часть большой задачи «полностью
переделать поведение стража по образцу Кида».

**Диагноз: большая переделка тут ни при чём, корень маленький и точный.**
Само возвращение стража в исходную позу — ПРАВИЛЬНОЕ поведение, порт верен;
не хватает ровно одного сигнала.

Разбор по SDLPoP:

1. **Выход Кида из комнаты «усыпляет» стража — так и в оригинале.**
   `leave_guard` (seg002:02F5) складывает стража обратно в данные уровня
   (тайл, `x`, **направление**, skill, HP), а `enter_guard` (seg002:0112)
   при возврате поднимает живого стража **с УБРАННЫМ мечом**
   (`sword_0_sheathed` + `seq_77_guard_stand_inactive`) и обнуляет
   `is_guard_notice`/`guard_refrac`.  То есть страж после возврата ВСЕГДА
   неактивен и смотрит в запомненную сторону — у нас так же
   (`pop_guard_enter`, `pop_guard.c:127`).
2. **Дальше решает `autocontrol_guard_inactive` (seg002:0876), и он Кида за
   спиной ИГНОРИРУЕТ.**  Кид вернулся справа, страж смотрит влево →
   `char_opp_dist()` отрицательна → ветка `else if (distance < 0) return;`.
   Единственный выход из неё — флаг **`is_guard_notice`**: «Кид нашумел».
   При нём страж оборачивается (`move_4_down`).  Направление в
   `check_can_guard_see_kid` НЕ участвует вовсе, так что «не видит» — это
   не про луч видимости, а именно про этот флаг.
3. **У нас `is_guard_notice` не взводится НИГДЕ.**  `grep` по всем исходникам
   roomtest: объявление (`pop_guard.c:20`), сброс (`pop_guard.c:47`) и одно
   чтение (`guards.c:171`).  Присваивания `= 1` нет ни одного — флаг мёртв,
   поэтому неактивный страж не обернётся НИКОГДА, что бы Кид ни делал.

**Где его взводит оригинал** (это и есть объём фикса):

| место | событие |
|-------|---------|
| `seg006:633..641` — `play_seq`, опкод SOUND | звук `SND_SILENT`(0), `SND_FOOTSTEP`(1), `SND_BUMP`(2).  `SND_DRINK`(3)/`SND_LEVEL`(4) — НЕ шум.  Главный источник: шаги бега/приземления |
| `seg004:05F1` `bumped_sound` | удар в стену |
| `seg005:185,195` | мягкое и среднее приземление (**только `charid_0_kid`**) |
| `seg006:1294`, `seg006:1734` | Кид обрушил loose-плиту (зацепом и наступив) |
| `seg007:766` | нажата кнопка |

У нас опкод SOUND в `play_seq` (`pop_kid.c:426`) просто съедает байт
аргумента — звука нет, и флаг вместе с ним потерялся.  Именно эта строка —
90 % фикса: `SND_SILENT` называется «silent» потому, что звука не издаёт,
**но стражи его всё равно замечают**, и в `seqtbl` он стоит, например, в
`ready` (доставание меча).

**Ожидаемое поведение после фикса.**  Кид возвращается в комнату 11 бегом →
первый же `SND_FOOTSTEP` взводит флаг → страж оборачивается и достаёт меч.
Стоя на месте, Кид может подкрасться к стражу со спины — это НЕ баг, а
механика оригинала.

**Оговорка (не проверено вживую).**  Разбор построен на том, что страж после
возврата неактивен (меч убран, кадр 166).  Это следует из кода
`pop_guard_enter`, но в MAME не снималось; если окажется, что меч у него
ОБНАЖЁН, то работает другая ветка (`autocontrol_guard_active`, где
`can_guard_see_kid == 2` направления не спрашивает) — и тогда корень другой.
Снять при фиксе: `Guard.sword`, `Guard.frame`, `can_guard_see_kid` сразу
после входа в комнату.

**Фикс (2026-08-05): все пять мест портированы.**  Опкод SOUND в `play_seq`
(`pop_kid.c`) взводит флаг для звуков 0..2; `bumped_fall`/`bumped_floor`,
мягкое и среднее приземление, обрушенная плита (все три пути `check_press`)
— в `pop_map.c`; щелчок кнопки — в `pop_trob.c`.  Заглушка `is_guard_notice`
добавлена в `tests-host/stubs.c` (автопилота стража в наборах нет).

**Как проверить в игре:** комната 11 уровня 2, дать стражу вытолкнуть Кида
вправо в комнату 22 и вернуться бегом — на первом же шаге страж обязан
обернуться и достать меч.  Подкрасться к нему СТОЯ (не двигаясь) по-прежнему
можно — это механика оригинала, а не баг.

---

<a id="bug-grab-1"></a>
## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ПРИЧИНА НАЙДЕНА, ФИКС ЕСТЬ, ЖДЁТ ИГРОВОЙ ПРОВЕРКИ**

> **Итог 2026-08-05.**  Физика тут ни при чём — виновата клавиатура.
> Зажатый Shift снимался автоповтором зажатой стрелки, поэтому к кадрам
> 102…106 (окно зацепа) движок видел Shift отпущенным.  Полный разбор и
> фикс — [BUG-KBD-5](bug_closed.md#bug-kbd-5); поведение Shift в MAME
> проверено замером карты `_kbdraw_down`.  Осталось подтвердить сам зацеп
> живой игрой; версии 2 и 3 ниже проверять только если он всё ещё не выйдет.


**Симптом.**  Уровень 2, комната 9.  Перепрыгнув на (1,1), Кид должен
вернуться обратно: разбегаться негде, поэтому он встаёт на самый край
плиты, прыгает с места и **цепляется руками за (1,5)**, после чего
подтягивается.  У нас Кид с зажатым Shift всё равно срывается.

**Что уже точно известно (и не надо перепроверять).**

1. **Физика прыжка у нас совпадает с оригиналом кадр в кадр.**  Сверено по
   логу SDLPoP против трассы харнесса при одинаковом старте `x=95`:

   ```
   кадр   16   18   22   23   24   25   102  103  104  105
   SDLPoP 95   97  105  112  121  126  128  130  131  133
   наш    95   97  105  112  121  126  128  130  131  133
   ```
   Совпадает и по `y`, и по колонке/ряду, и по приземлению на 107–108.

2. **В оригинале зацеп срабатывает на кадре 106, а не 102..105.**
   `check_grab` зовётся из ДВУХ мест: ветка «в воздухе» в `check_action`
   (кадры 102..105) и `do_fall` (seg005) для `actions_4_in_freefall`.
   Успешная попытка из лога:

   ```
   GRAB try  f=106 x=135 y=166 col=4 row=2 fall_y=18
   GRAB probe x=127 col=4 through=0 front_above=3 modif=0
   GRAB      can_grab=1
   GRAB OK   dist=9
   -> f=91 x=136 y=181 col=5 row=2 act=2   (повис)
   ```
   Наш `do_fall` (`pop_map.c`) `check_grab()` из этой ветки тоже зовёт —
   то есть структура на месте, расходится что-то внутри.

3. **По харнессу зацеп у нас РАБОТАЕТ**: окно стартовых `x = 91…95`, и
   короткий шаг ставит Кида ровно туда (91 после первого нажатия, 95 после
   второго).  Зафиксировано тестом `t_grab`.

**Отсюда главный вопрос был: почему харнесс говорит «работает», а живая
машина — «нет».**  Расхождение между ними и оказалось уликой; версии
выдвигались по убыванию правдоподобия, и сработала первая:

- **Shift не доезжает до движка — ПОДТВЕРЖДЕНО, это и была причина.**
  Харнесс подменяет клавиатуру и потому этот путь не проверяет вовсе, а у
  нас есть история проблем ровно с «Shift + стрелки» (KBD-1, BUG-KBD-3/4).
  Замер в MAME: при зажатом Shift и зажатой стрелке бит `LSh` в
  `_kbdraw_down` стоял в нуле.  Разбор — [BUG-KBD-5](bug_closed.md#bug-kbd-5).
- **Сцена харнесса не равна комнате 9.**  Там изолированная комната
  (соседи — стена), а в игре слева комната 8; кромки шва участвуют в
  `get_tile`.  Проверять чтением `Kid.x` в момент прыжка: попал ли он в
  окно 91…95 вообще.
- **Расхождение в `check_grab`.**  Наш вариант зовёт `determine_col()`
  там, где оригинал зовёт `load_fram_det_col()` (перезагрузка кадра +
  колонка).  Для Кида это обычно одно и то же (`cur_frame` в фазе физики
  принадлежит ему), но проверить стоит.

**Инструменты готовы.**  В SDLPoP включена отладка (пометка `DBG-GRAB`):
`JMP` — покадровая трасса прыжка/падения/виса, `GRAB try|probe|fail|OK` —
вход в `check_grab` и причина отказа.  Снимается поиском по `DBG-GRAB`.

**Найдено попутно, отдельным наблюдением.**  После касания площадки на
кадрах 107–108 (x=140) оба движка снова падают, но X расходится: SDLPoP
уводит Кида на 134 (колонка 4), мы — на 141 (колонка 5).  Похоже на разную
отработку `in_wall()` у стены (2,7).  На зацеп не влияет.

---

<a id="bug-gatemod-1"></a>
## BUG-GATEMOD-1. Ворота стартуют закрытыми, хотя в уровне открыты — **ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ**

**Симптом (пользователь, 2026-08-04).**  Уровень 2, комната 13: решётка
между (2,5) и (2,6) обязана быть ОТКРЫТА в начале и захлопнуться, когда Кид
нажмёт кнопку (2,4) — после этого назад дороги нет.  У нас она закрыта
сразу, кнопка бессмысленна, проход не работает.

**Корень.**  Модификатор тайла в ФАЙЛЕ уровня и модификатор в РАНТАЙМЕ —
разные величины; оригинал переводит их при загрузке в `load_alter_mod`
(seg008:198E), которую зовёт `alter_mods_allrm` из `load_level`:

```c
case tiles_4_gate:  *modif = (*modif == 1) ? 188 : 0;  break;
case tiles_11_loose:*modif = 0;                        break;
case tiles_10_potion:*modif <<= 3;                     break;
```

Наш `pop_trob_modif` портировал из неё **только зелье**.  Для ворот
`bg = 1` — это «открыты» (Table 8 спецификации DAT), а в рантайме открытость
измеряется высотой подъёма 0..188; мы клали в рантайм-модификатор сырую
единицу, то есть «закрыты на 1/188».

**Фикс.**  Ветки ворот и loose дописаны в ленивую инициализацию
`pop_trob_modif` (`pop_trob.c`).  Ветка СТЕН не портируется намеренно: у нас
`pop_bg` считает связи кладки по типам соседей прямо при отрисовке
(`wall_modifier`), сохранённый модификатор стены не читается.

**Что это ещё задевает.**  Решётка (0,9) комнаты 5 уровня 1 тоже имеет
`bg = 1`, то есть обязана стартовать открытой — Кид сваливается в комнату 1
именно через неё, и она захлопывается у него за спиной.  Закрывает её
стартовый триггер `do_startpos` (seg003:167): для уровней с
`tbl_entry_pose == 1` оригинал ВИРТУАЛЬНО ЖМЁТ кнопку комнаты 5 (0,2) —

```c
// Special event: press button + falling entry
get_tile(5, 2, 0);  trigger_button(0, 0, -1);  seqtbl_offset_char(seq_7_fall);
```

Замер в SDLPoP (лог по кадрам): `gate(5,0,9)` идёт `188 → 148 → 88 → 8 → 0`,
шаги 40/60/80 — это `gate_close_speeds`, то есть быстрое закрытие
(`trigger_gate` вернул тип 3).  У нас этот триггер портирован, и закрытие
работает.

Полный список ворот с `bg = 1`: ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5).
Остальные ворота уровней 1–3 имеют `bg = 2` → 0, и для них ничего не
меняется (при модификаторе 2 и 0 и отрисовка, и `can_bump_into_gate` дают
одно и то же).

**Побочная находка: чит обхода комнат отматывал мир.**  После фикса
пользователь увидел «ворота снова открылись», пройдя `+` в комнату 2 и `-`
обратно.  Причина не в воротах: `ROOMNAV` звал `pop_trob_reset()` перед
`enter_room`, тот обнулял `room_seen[]`, и `pop_trob_modif()` перечитывал
модификаторы из уровня заново — то есть чит откатывал открытые/закрытые
ворота, выдвинутые пики и нажатые кнопки.  Пока ворота с `bg=1` ошибочно
стартовали закрытыми, откат был не виден.  `pop_trob_reset()` из навигации
убран: она обязана только телепортировать, исходное состояние даёт
перезапуск уровня.  Замер, который это показал: `room_modif` комнаты 5
после `+`/`-` = `00 00 0B 00 09 00 08 01 00 BC` — последний байт 0xBC = 188,
файловое значение.

---


<a id="ручная-перепроверка-2026-08-03"></a>
# Ручная перепроверка фиксов (2026-08-03)

Шесть корней закрыты автоматической проверкой в MAME (мост `mame-z80`:
чтение `_Kid`, потиковые трассы, скриншоты) — этого достаточно, чтобы
показать, что конкретный сценарий больше не воспроизводится, но НЕ
достаточно, чтобы поймать регрессии в соседней механике.  Ниже — сценарии
ровно в тех формулировках, в которых баги были заведены.

Собрано и лежит в образе (`make` + `make hdd` уже прогнаны; libc пересобран,
`make size-check` без роста).

| # | что проверить | ожидаемо |
|---|---------------|----------|
| 1 | комната 22: выпить зелье (0,6), выйти в 16/23 и вернуться | кувшина нет, пузырька над пустым местом нет |
| 2 | то же для комнат 14 (0,5) и 17 (2,3) | так же |
| 3 | комната 15: подобрать меч (2,2), выйти и вернуться | меча нет; кладка на дальней стене НЕ мигает |
| 4 | комната 12: разбить плиты (0,1)/(0,2) и потолок в 16, выйти-вернуться | остаются разбитыми, проём не закрывается |
| 5 | комната 17 из 23: разбить (1,5)/(1,6), выпить зелье (2,3), вернуться | всё остаётся |
| 6 | **после смерти** зайти в те же комнаты | ВСЁ восстановлено (в оригинале смерть = `load_level`) |
| 7 | комната 12: разбег в закрытую решётку (0,9) с полушага | не проходит насквозь; перелистывание экрана штатно (см. BUG-SEAM-DRAW-1 в `bug_closed.md`) |
| 8 | комната 6: бег справа налево от (0,9), длинный прыжок (0,6)→(0,7) | не влетает внутрь стены |
| 9 | комната 5: с кнопки (0,6) падение на (2,7), присед, вставание | остаётся в комнате 5 (проверено трассой: `fr=111 x=177` → `seq_47` → `fr=15 x=173`) |
| 10 | комната 5: нажать кнопку (0,4) | поднимаются ОБЕ решётки — (0,5) видно на экране, (0,9) проверять из комнаты 1 |
| 11 | страж (комнаты 3, 21) убивает Кида | смерть доигрывается, респавн в стартовой позиции уровня; цикла «убил-воскрес» нет |
| 12 | клавиатура: долгая игра с Shift+стрелка | ↑ и Shift не залипают; осторожный шаг не превращается в бег (доработано во второй волне — п.17) |

## Вторая волна (2026-08-03, вечер)

| # | что проверить | ожидаемо |
|---|---------------|----------|
| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки |
| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные |

Отдельно стоит посмотреть **на регрессии от порта `check_collisions`** — он
трогает всю горизонтальную коллизию: бамп в стену на бегу и в прыжке,
осторожный шаг у стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме
решётки, переходы через швы (старый BUG-SEAM-PINGPONG).

---

# Открытые баги уровня 1

<a id="bug-gate-pass-1"></a>
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ

**Статус: наблюдался один раз, воспроизвести повторно НЕ удалось.**  Заведён,
чтобы наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
«не баг», пока нет надёжного сценария.

**Что наблюдалось (2026-08-03).**  Комната 5: Кид стоял НА тайле решётки (0,9)
и ждал, пока она опустится.  После закрытия пошёл вправо — прошёл в комнату 1
и упал на (1,1).

**Что уже измерено и в чём загвоздка.**  Сразу после наблюдения повторить не
получилось: в том же месте Кид стоит на `x = 196`, `col = 9`, и решётка его
ДЕРЖИТ — то есть штатно.

Арифметика оригинала объясняет разницу.  `is_obstacle` (seg004) ставит
плоскость блокировки в `x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX`, для
колонки 9 это **x = 205**.  При этом «колонка 9» по `get_tile_div_mod_m7` —
это `x ∈ [191, 205)`.  Пока `curr_col == 9`, Кид гарантированно левее
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться **правее
205**, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
весь тайл.

Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
по тайлу дальше, пока решётка была поднята), и поведение штатное.  Но повторить
эту позу и снять `x` пока не удалось, поэтому гипотеза НЕ подтверждена.

**Что снять в следующий раз** (без этих чисел вопрос не закрыть):

1. `Kid.x` и `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё
   стоит на её тайле — до шага вправо;
2. модификатор решётки (openness) комнаты 5, тайл 9 — `can_bump_into_gate()`
   считает её препятствием только пока `(modif >> 2) + 6 < char_height`, то
   есть пока она опустилась достаточно низко относительно РОСТА кадра;
3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.

Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
продолжить), затем чтение `_Kid` из отладчика MAME.

**Возможный корень, если гипотеза не подтвердится.**  Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
(ср. закрытый BUG-SEAM-PINGPONG).  Второй кандидат — `char_height` в
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени.

# Оптимизация отрисовки (записано 2026-07-29)

Не баги — план работ.  Оба пункта про одно: у оригинала пометка тайла к
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
кадр), а у нас каждая такая пометка превращается в реальный heal (копию из
ОЗУ-копии акселератора) плюс блиты.  Поэтому буквальный порт «перерисовываем
безусловно» корректен, но дорог.

<a id="t-1"></a>
## T-1. Пики: перерисовывать по причине, а не безусловно

**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
каждой живой пики в комнате (порт `redraw_21h`, который `animate_spike`
вызывает вне всяких `if`).  Это корректно, но лишнее для пик, до которых
Киду дела нет.

**Надо:** перерисовывать тайл пики, только если
1. **сменился её видимый кадр** (шаг выдвижения/уборки), ЛИБО
2. **её кто-то стёр** — а стереть у нас может только heal, то есть тайл
   попал в прямоугольник `kid_heal` этого кадра.

Это и есть модель оригинала, просто выраженная флагами: `redraw_at_char`
(seg003:0576) каждый кадр помечает `set_redraw_fore` тайлы персонажа, причём
**объединение текущего и предыдущего** прямоугольника
(`MIN(char_top_row, prev_char_top_row)` и т.д.), а `animate_spike` помечает
свой тайл.  Итог = {тайл сменил кадр} ∪ {тайлы Кида}.

**Как:** слой Кида и так считает `cL..cR`/`rT..rB` в `pop_fore_over_kid` —
пусть публикует их (плюс предыдущие, как в оригинале), а цикл trob'ов
сравнивает `tilepos` с диапазоном целочисленно.  Никаких пересечений
прямоугольников (см. память `manual_hints_over_auto_detect`).

**Приоритет:** отдаётся почти бесплатно ПОСЛЕ T-2, отдельно не окупается.

<a id="t-2"></a>
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит

**Сейчас:** `kid_heal` → `kid_draw` → `pop_fore_over_kid` идут каждый кадр,
даже когда Kid стоит и в его тайлах ничего не меняется.  Это ровно поведение
оригинала (`draw_game_frame`, seg000:917 — `draw_moving()` + `draw_tables()`
безусловно), но у него это дёшево, а у нас нет.

**Надо:** пропускать heal+draw Кида, когда кадр/поза/координаты не менялись
и в его тайлах нет активной анимации.

**Осторожно (дабл-буфер):** пропускать можно **не раньше второго подряд**
неизменного кадра — иначе одна из двух страниц останется со старым
содержимым.  Условие «обе страницы уже получили это состояние».

**Связь с T-1:** после T-2 пики отпадают сами — раз Кида не перерисовываем,
heal'а нет, стирать пики нечем, редрой не нужен.

**Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно
там, где тапают Shift+стрелку (см. остаток KBD-1 в [`TASKS.md`](TASKS.md)).

---

# Обход всех 24 комнат уровня 1 — поиск багов отрисовки

Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо
`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой),
Kid ставится на первый пол, `pop_trob_reset()` возвращает пики/ворота в
исходное.  Номер комнаты — полосками в верхнем борте: слева десятки, справа
единицы (`||` `||||` = 24).  Убирается закомментированием одной строки.

Комнаты **13, 18, 24 недостижимы** в обычной игре — это свойство данных
уровня, разбор в [`bug_closed.md`](bug_closed.md) («НЕ БАГИ»).  Приоритет
багов в них низкий.

Таблица заполнена на 5 строк из 24 — доделать в рамках L1-TRIAGE
([`TASKS.md`](TASKS.md)).

| комната | статус | что не так |
|---------|--------|------------|
| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты |
| 2 |  |  |
| 3 |  |  |
| 4 |  |  |
| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` |
| 6 |  |  |
| 7 |  |  |
| 8 |  |  |
| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43).  Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) |
| 10 |  |  |
| 11 |  |  |
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом.  Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0).  Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» в `bug_closed.md` |
| 13 | недостижима в игре | |
| 14 |  |  |
| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован.  Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y−3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
| 16 |  |  |
| 17 |  |  |
| 18 | недостижима в игре | |
| 19 |  |  |
| 20 |  |  |
| 21 |  |  |
| 22 |  |  |
| 23 |  |  |
| 24 | недостижима в игре | |

---

## Заметки (отладка)

- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
  (эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`.  Первый шаг в
  сторону = разворот (как в оригинале safe_step), движение со второго.
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
  **S** — выдать меч.
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
  Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
  `S`.  Все они со временем съедутся в общий блок читов, разрешаемый в
  настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).


---

<a id="сырые-наблюдения-прогон-2026-08-03"></a>
## Сырые наблюдения (прогон 2026-08-03) → корень

Формулировки с прогона, как записаны.  Закрытые корни — в
[`bug_closed.md`](bug_closed.md).

| # | наблюдение (кратко) | корень | статус |
|---|---------------------|--------|--------|
| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт |
| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт |
| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт |
| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт |
| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт |
| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт |
| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) |
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) |
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт |
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт |
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт |
