diff --git a/applications/PoP/roomtest/BUGS_CLOSED.md b/applications/PoP/roomtest/BUGS_CLOSED.md
index cc2be16..887daf4 100644
--- a/applications/PoP/roomtest/BUGS_CLOSED.md
+++ b/applications/PoP/roomtest/BUGS_CLOSED.md
@@ -13,6 +13,69 @@
---
+
+## GRAB-BELOW-ROOM. Зацеп за кромку НИЖНЕГО ряда при пролёте вниз не работал вовсе
+
+**Наблюдение (пользователь, 2026-08-12).** Уровень 7, комната 14: по сценарию
+уровня Кид спускается с ряда 0 на ряд 2 через зацеп — виснет на кромке кнопки
+`(0,2)`, отпускает и в падении цепляется за кромку `(2,2)`. «Пытался много
+раз — Кид летит и не цепляется ни за `(2,2)` комнаты 14, ни за `(1,2)` комнаты
+15». При этом вис на `(2,2)` с последующим перелётом на `(1,2)` **работает
+стабильно** — то есть сам зацеп в падении жив.
+
+**Корень.** `do_fall` (порт seg005:0030) не давал ряду персонажа выйти за
+нижнюю границу комнаты:
+
+```c
+else if (Char.curr_row < 2)
+ inc_curr_row(); /* «следующий ряд ВНУТРИ комнаты» */
+```
+
+В оригинале `inc_curr_row()` безусловный, и `curr_row` доходит до **3** — ряда
+«за нижней кромкой»; `get_tile` для него уходит по `links.down` в комнату снизу
+(`find_room_of_tile`, seg006:005D). Ряд 3 нужен не сам по себе, а потому, что
+`check_grab` целится в тайл **спереди-сверху**, то есть в ряд `curr_row − 1`:
+только при `curr_row == 3` целью становится ряд 2 своей комнаты.
+
+С залипшим рядом 2 происходило вот что: пока Кид летел вдоль нижнего ряда,
+`do_fall` каждый кадр уходил в ветку «достиг `y_land`», где `check_grab` не
+вызывается вовсе. К моменту, когда переход в комнату снизу открывал окно
+зацепа заново (там цель — «ряд −1», уже реализованный `g_above`), скорость
+падения успевала дойти до `fall_y = 33`, а `check_grab` требует `< 32`. Окно
+закрывалось **по скорости** — отсюда «летит и не цепляется никак».
+
+Второй дефект, найденный там же: ветка `action == ACT_MIDAIR` в `check_action`
+стояла ПУСТОЙ заглушкой с комментарием «frames 102..105: check_grab — K4». В
+оригинале (seg006:0619) это первые четыре кадра падения, где `fall_y` ещё не
+разгоняется (`fall_accel` работает только в `ACT_FREEFALL`), — самое широкое
+окно зацепа. У нас его не было.
+
+**Фикс** (`pop_map.c`): `inc_curr_row()` теперь безусловный внутри ветки
+`curr_row <= 2` (то есть 2 → 3 разрешён), а сама ветка «достиг `y_land`»
+гейтится по `curr_row <= 2` — при ряде 3 ни `in_wall`, ни `land` звать нельзя
+(тайлов своей комнаты там нет, `get_tile` отдаёт `WALL`-сентинел; до
+`y_land[4] = 244` дело не доходит, `check_leave_below` переводит в комнату
+снизу на `y >= 211`). Плюс восстановлена ветка `ACT_MIDAIR` с кадрами 102..105.
+
+**Подтверждение.**
+- Хост-тест `tests-host/t_grab.c` → `grab_below_room_edge_window_exists`:
+ сцена комнаты 14 + переход в 15 по `pop_fell_out` (как в главном цикле).
+ Карта исходов по фазе X и задержке Shift **до** фикса — сплошные точки (ни
+ одного зацепа), **после** — окно из 8 фаз X при задержках 0..5 кадров.
+ Тест проверяет и играбельность: зацеп обязан удаваться при самом
+ естественном вводе (отпустил вис и сразу зажал Shift).
+- Живьём в MAME (мост, 2026-08-12): Кид повис на кромке `(2,2)` — в памяти
+ `frame=91 y=55 row=0 room=15 act=6`, на экране держится за правый край плиты.
+
+**Грабли теста, стоившие часа.** Первая версия сцены засчитывала как успех
+ЛЮБОЙ вис — а Кид на первых кадрах падения цепляется обратно за ту же верхнюю
+кромку, и тест проходил даже на сломанном коде. Признак цели пришлось делать
+позиционным (`went_below` + `curr_row == 0` после перехода). Вторая ловушка:
+кадры спуска `seq_68` идут с `action == 3`, то есть «дождаться падения» по
+одному лишь `action` нельзя — ждать надо ВИСА, и только потом отпускать Shift.
+
+---
+
## BUG-CHEAT-FIGHT-1. Чит `+`/`−` в бою: Кид остаётся в режиме боя и теряет управление
diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md
index 6cf1f88..6ca2b28 100644
--- a/applications/PoP/roomtest/BUGS_OPEN.md
+++ b/applications/PoP/roomtest/BUGS_OPEN.md
@@ -22,6 +22,41 @@
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
+| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
+
+---
+
+
+## GRAB-KBD-TIMING. Зацеп в падении срабатывает НЕ ТАК СТАБИЛЬНО, как в оригинале — РАЗБОР ПОСЛЕ ВСЕХ УРОВНЕЙ
+
+> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
+> Механика работает, это вопрос ощущения, а не проходимости.
+
+**Наблюдение (пользователь).** Вход на уровень 7: Кид влетает в комнату сверху
+и, пролетая мимо края пола, МОЖЕТ за него зацепиться — но получается заметно
+реже, чем в оригинале. «Похоже, это проблемы нашего клавиатурного модуля».
+
+**Что уже сделано и почему это НЕ закрывает вопрос.** В ходе разбора
+[GRAB-BELOW-ROOM](BUGS_CLOSED.md#grab-below-room) восстановлена ветка
+`ACT_MIDAIR` в `check_action` (кадры 102..105 — начало падения, где `fall_y` ещё
+не разогнан): раньше на этих четырёх кадрах `check_grab` не звался вовсе, то
+есть окно зацепа было короче оригинального. Это должно было добавить
+стабильности, но саму гипотезу про клавиатуру не проверяет.
+
+**Куда смотреть, когда дойдут руки.** Порядок именно такой — сначала
+измерить, потом чинить:
+
+1. Снять, ЧТО видит движок в момент падения: `pop_ctrl_shift_held()` покадрово
+ (Shift зажат заранее — читается ли он каждый кадр, или FIFO SIO отдаёт
+ состояние с пропусками; см. memory `kbd_raw_fifo_drain`,
+ `kbd_overrun_wipe_modifiers` — оба прошлых бага были именно про модификаторы).
+2. Сверить с оригиналом ширину окна по кадрам: у нас `check_grab` доступен на
+ 102..105 (midair) + весь `ACT_FREEFALL`, как в seg006 — расхождений в коде
+ быть не должно, значит расхождение либо во вводе, либо в темпе игры
+ ([L1-SPEED](TASKS_OPEN.md#l1-speed): игра идёт на ~39 % быстрее оригинала,
+ а окно зацепа отмеряется В КАДРАХ — то есть по времени оно у нас короче).
+3. Хост-тест `t_grab.c` уже умеет мерить ширину окна (карта исходов по фазе X и
+ задержке нажатия) — на нём и проверять «стало стабильнее».
---
diff --git a/applications/PoP/roomtest/NEXT_SESSION.md b/applications/PoP/roomtest/NEXT_SESSION.md
index 8577cdf..ff193cf 100644
--- a/applications/PoP/roomtest/NEXT_SESSION.md
+++ b/applications/PoP/roomtest/NEXT_SESSION.md
@@ -1,4 +1,4 @@
-# Точка входа для следующей сессии (обновлено 2026-08-11, вечер)
+# Точка входа для следующей сессии (обновлено 2026-08-12)
Файл для старта с чистого контекста: где всё стоит, что делать первым, какие
грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам:
@@ -6,22 +6,24 @@
---
-## 1. СЛЕДУЮЩАЯ ЗАДАЧА: зацеп в ПАДЕНИИ (вход на уровень 7)
+## 1. СЛЕДУЮЩАЯ ЗАДАЧА: зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1)
-Уровень 6 пройден и принят: спецсобытие с тенью работает, переход на уровень
-7 падением работает. Но **вход на 7-й ломается на механике**: Кид влетает в
-комнату сверху и, пролетая мимо края пола, ДОЛЖЕН иметь возможность
-зацепиться за него руками. Похоже, этого у нас нет вовсе — проверить и
-реализовать.
+План готов и лежит в [`TASKS_OPEN.md#l7-feather`](TASKS_OPEN.md#l7-feather) —
+шесть шагов с уже выписанными константами оригинала (ускорение 1 / потолок 4,
+`SEQ_JMP_IF_FEATHER` → ветки `stepfloat`/`bumpfloat`, зелёная вспышка, зелёные
+пузырьки, таймер 18,75 с). Начинать прямо с шага 1.
-Куда смотреть: `check_grab` / ветка `action == actions_3_in_midair` в
-seg005-seg006 SDLPoP (у нас `POP_ENABLE_JUMP_GRAB` и `check_grab_run_jump`
-в `pop_map.c` — это ДРУГОЙ случай, зацеп из прыжка). Начинать, как всегда,
-с чтения оригинала, а не с догадок.
+**Зацеп в падении — закрыт 2026-08-12**
+([GRAB-BELOW-ROOM](BUGS_CLOSED.md#grab-below-room)). Оказалось, что зацеп
+из падения был жив, а сломан был спуск через НИЖНЮЮ кромку комнаты (уровень 7,
+комната 14): `do_fall` не пускал ряд персонажа до 3, и `check_grab` на этом
+отрезке не звался ни разу. Плюс восстановлена ветка `ACT_MIDAIR` (кадры
+102..105 — начало падения). Проверено хост-тестом `t_grab` и живьём в MAME.
-Опора: вход падением уже портирован — `FALLING_ENTRY_LEVEL/ROOM` в
-`pop_guard.h` + ветка в `pop_start_level` (`roomtest.c`), Кид появляется в
-комнате 17 и экран сразу переводится на комнату под ней.
+Осталась ощущенческая часть — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing):
+зацеп при входе на уровень 7 срабатывает менее стабильно, чем в оригинале
+(подозрение на клавиатурный модуль и на общий темп игры, `L1-SPEED`).
+**Решение пользователя: разбирать после того, как будут готовы все уровни.**
## 2. Состояние репозитория
diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md
index c85b6d7..3875f75 100644
--- a/applications/PoP/roomtest/TASKS_OPEN.md
+++ b/applications/PoP/roomtest/TASKS_OPEN.md
@@ -87,6 +87,56 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
## P0 — делаем сейчас
+### L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1)
+
+**Задача (пользователь, 2026-08-12): составить план — здесь он и есть; код
+следующим заходом.**
+
+Зелье на `(2,8)` комнаты 1 уровня 7 (сверено по `res2007.bin`: modifier 3, то
+есть `potion_type = 3` — «slow fall»). Сейчас `pop_proc_get_object`
+(`pop_map.c`) для типов 3/4/6 не делает НИЧЕГО (явный TODO): зелье выпивается
+без эффекта.
+
+Механика оригинала (прочитана в SDLPoP до планирования, правило проекта):
+
+| что | где в SDLPoP | значение |
+|---|---|---|
+| включение | `feather_fall()`, seg000:15F8 | `is_feather_fall = 1`, зелёная вспышка (`flash_color = 2`, `flash_time = 3`), `stop_sounds` + `sound_39_low_weight` |
+| физика | `fall_accel()`, seg006:057C | ускорение **1** вместо 3, потолок скорости **4** вместо 33 (`FALLING_SPEED_*_FEATHER`, types.h:1435) |
+| анимация | опкод `SEQ_JMP_IF_FEATHER`, seg006:586 | в seqtbl есть ветки `stepfloat` / `bumpfloat` — «плавные» кадры падения и удара; таблица у нас из данных оригинала, ветки УЖЕ ЛЕЖАТ в ней |
+| длительность | `do_timers`, seg003:517 | ваниль: пока играет звук (или 225 тиков); фикс SDLPoP `fix_quicksave_during_feather` — таймер `FEATHER_FALL_LENGTH = 18.75 c` |
+| сброс | seg003:189 (`start_level`) | на старте уровня и, по фиксу, при смерти Кида |
+| вид склянки | `draw_tile_fore`, seg008:740 | типы 2..4 — БОЛЬШАЯ склянка (id 13) — **у нас уже так** |
+| цвет пузырьков | `draw_tile_anim`, seg008:652 | типы 3/4 — **зелёный** (`color = 10`), 5/6 — синий, остальные — красный (12) |
+
+Шаги (в порядке выполнения, каждый проверяем отдельно):
+
+1. **Состояние.** `pop_feather` (счётчик кадров) в `pop_map.c` рядом с
+ `fall_accel`; экспорт в `pop_map.h` для `play_seq`. Длительность — по
+ таймеру (ванильная привязка к звуку нам не подходит: звука нет), значение
+ пересчитать из 18,75 с в наши кадры и завести в `pop_tune.h`. Сброс — в
+ `pop_start_level` и по смерти Кида.
+2. **Физика.** `fall_accel()`: при `pop_feather` — `FALL_ACCEL_FEATHER 1` /
+ `FALL_MAX_FEATHER 4`. Только для Кида (`charid == CHARID_0_KID`) — так
+ правильнее по смыслу, но это ОСОЗНАННОЕ расхождение с ванилью (там эффект
+ ловят все, и SDLPoP чинит это опцией) → запись в `../docs/impl_diff.md`.
+3. **Анимация.** Опкод `0xF7` в `play_seq` (`pop_kid.c`) сейчас БЕЗУСЛОВНО
+ пропускает адрес; сделать как в оригинале: при `pop_feather` — прыжок по
+ адресу (это и даёт `stepfloat`/`bumpfloat`, то есть отсутствие урона и
+ «парение»). Правка на 3 строки, но именно она даёт весь визуальный эффект
+ падения.
+4. **Вспышка.** Сейчас цвет вспышки — булев `pop_flash_red` (жёлтая/красная);
+ расширить до кода цвета и добавить ЗЕЛЁНУЮ (`flash_time = 3`).
+5. **Зелёные пузырьки.** `pop_pack_bg.py` уже красит пузырёк mono-цветом
+ (`POT_BUBBLE_COLOR = VGA16 + 12`, красный). Добавить второй набор кадров
+ 16..22 в зелёном (`+10`) под своими id в атласе `pop_pot` и выбирать набор
+ по `potion_type` в `pop_potion_draw` (`pop_room.c`): 3/4 — зелёный, 5/6 —
+ синий, иначе красный. Цена — 7 маленьких спрайтов.
+6. **Проверка.** Хост-тест: падение с трёх рядов под пером — скорость не выше
+ 4, урона нет, Кид жив (сцена в `t_phys`/`t_char`). MAME: комната 1 уровня
+ 7 — выпить, спрыгнуть в шахту, убедиться в плавном спуске и в том, что
+ эффект кончается по таймеру.
+
### L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный)
**Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В
diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h
index 8bef336..e3c3fe0 100644
--- a/applications/PoP/roomtest/pop_guard.h
+++ b/applications/PoP/roomtest/pop_guard.h
@@ -114,6 +114,13 @@
#define FALLING_ENTRY_LEVEL 7
#define FALLING_ENTRY_ROOM 17
+/* Комната, которую проходят СВЕРХУ ВНИЗ спуском через зацеп: Кид висит на
+ * кромке кнопки (0,2), отпускает и цепляется за кромку (2,2). Нужна ЧИТУ
+ * навигации (`+`/`−` в roomtest.c): он ставит Кида на нижний пол, а отсюда
+ * этот спуск не проверить — для этой комнаты старт в ряду 0. */
+#define GRAB_DOWN_LEVEL 7
+#define GRAB_DOWN_ROOM 14
+
/* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех
* остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см.
* pop_check_can_guard_see_kid. */
diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c
index c1a0157..6234863 100644
--- a/applications/PoP/roomtest/pop_map.c
+++ b/applications/PoP/roomtest/pop_map.c
@@ -797,17 +797,27 @@ static void do_fall(void)
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
check_grab(); /* ещё летит — попытка зацепа */
- } else {
+ } else if (Char.curr_row <= 2) {
if (get_tile_at_char() == TILE_WALL)
in_wall();
if (tile_is_floor(g_curr_tile))
land();
- else if (Char.curr_row < 2)
- inc_curr_row(); /* следующий ряд ВНУТРИ комнаты */
- /* curr_row==2, нет пола: продолжает падать (fall_speed двигает y);
- * пересечение нижней границы комнаты ловит check_leave_below (y>=211,
- * порт leave_room) — общий путь для провала И спуска-зацепа. */
+ else
+ inc_curr_row(); /* следующий ряд, ВКЛЮЧАЯ 2 -> 3 */
+ /* Ряд 3 — «за нижней кромкой комнаты» (в оригинале get_tile уходит по
+ * links.down и это ряд 0 комнаты снизу, seg006:005D). Нам он нужен
+ * ровно за тем, за чем нужен оригиналу: пока Kid летит вдоль ряда 3,
+ * check_grab пробует зацеп за тайл ряда 2 СВОЕЙ комнаты — иначе
+ * спуск «повис на кромке, отпустил, зацепился этажом ниже» через
+ * нижнюю границу комнаты невозможен в принципе (BUG-GRAB-SEAM-DOWN).
+ * Раньше ряд залипал на 2, check_grab не звался ни разу, и Kid
+ * пролетал мимо кромки, набирая fall_y > 32 — после чего окно
+ * зацепа закрыто уже по скорости. */
}
+ /* curr_row == 3: тайлов своей комнаты там нет (get_tile отдаёт WALL как
+ * сентинел), поэтому ни in_wall, ни land здесь звать нельзя — до y_land[4]
+ * дело всё равно не доходит: check_leave_below переводит в комнату снизу
+ * на y >= 211 (порт leave_room). */
}
static void fall_accel(void)
@@ -1310,7 +1320,13 @@ static void check_action(void)
} else if (action == ACT_FREEFALL) {
do_fall();
} else if (action == ACT_MIDAIR) {
- /* frames 102..105: check_grab — K4 */
+ /* Кадры 102..105 — НАЧАЛО падения (seg006:0619). Здесь fall_y ещё
+ * не разгоняется (fall_accel работает только в ACT_FREEFALL), то есть
+ * это САМОЕ ШИРОКОЕ окно зацепа: ветка стояла заглушкой, и первые
+ * четыре кадра падения зацеп не работал вовсе — отсюда «иногда
+ * цепляется, иногда нет» на входе в уровень 7. */
+ if (frame >= 102 && frame < 106)
+ check_grab();
} else if (action != ACT_HANGCLIMB) {
check_on_floor();
}
diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c
index a124d68..1fca257 100644
--- a/applications/PoP/roomtest/roomtest.c
+++ b/applications/PoP/roomtest/roomtest.c
@@ -696,15 +696,25 @@ int main(void)
* это и «прилёт» в проём/ворота, и, на уровне 5, посадка
* вплотную к тени спецсобытия, которая ждёт в колонке −1
* ряда 0. Вместо сцены кражи зелья начиналась схватка,
- * которой в этой комнате быть не должно.
+ * которой в этой комнате быть не должно. Снизу вверх нужно
+ * и самой навигации: в верхнем ряду пола чаще нет, и Кид
+ * проваливается из комнаты сразу после телепорта.
+ *
+ * ИСКЛЮЧЕНИЕ — комната 14 уровня 7 (пользователь 2026-08-12):
+ * её проходят СВЕРХУ ВНИЗ (с ряда 0 на ряд 2 через зацеп за
+ * кромки), и с нижнего ряда этот спуск не проверить вовсе.
+ * Пол в ряду 0 там есть, так что Кид не проваливается.
*
* В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его
* там нет — любой проходимый tile_is_floor (seg001: всё,
* кроме пустоты, верха колонны, верха двери, стены и
* «половинок» 26..29). */
{
- int8_t row;
- for (row = 2; row >= 0 && any == 0xFF; row--) {
+ int8_t row, step = -1, first = 2;
+ if (pop_current_level == GRAB_DOWN_LEVEL &&
+ r == GRAB_DOWN_ROOM) { first = 0; step = 1; }
+ for (row = first; row >= 0 && row <= 2 && any == 0xFF;
+ row = (int8_t)(row + step)) {
uint8_t fallback = 0xFF;
for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) {
c = (uint8_t)(room_fg[i] & 0x1F);
diff --git a/applications/PoP/roomtest/tests-host/t_grab.c b/applications/PoP/roomtest/tests-host/t_grab.c
index 4efac36..0d64355 100644
--- a/applications/PoP/roomtest/tests-host/t_grab.c
+++ b/applications/PoP/roomtest/tests-host/t_grab.c
@@ -149,11 +149,135 @@ TC_TEST(grab_same_for_all_key_patterns)
}
}
+/* ---- Спуск через НИЖНЮЮ КРОМКУ комнаты (BUG-GRAB-SEAM-DOWN) ---------- *
+ * Уровень 7, комната 14 — по ней уровень и проходят: Кид на ряду 0 висит на
+ * кромке кнопки (0,2), отпускает и в падении должен зацепиться за кромку
+ * (2,2). Тайл-цель лежит В НИЖНЕМ ряду, то есть в момент зацепа персонаж
+ * уже НИЖЕ комнаты — в оригинале это curr_row == 3, а get_tile для него
+ * уходит по links.down в комнату снизу (find_room_of_tile, seg006:005D).
+ *
+ * Пока do_fall не пускал ряд дальше 2, check_grab на этом отрезке не звался
+ * НИ РАЗУ: Кид пролетал мимо кромки, набирал fall_y > 32, и дальше окно
+ * зацепа было закрыто уже по скорости — спуск становился непроходимым.
+ * Тест держит именно это: окно существует и достижимо. */
+#define C 6 /* closer */
+#define O 15 /* opener */
+
+static const uint8_t room14[30] = {
+ C, F, O, E, E, E, E, E, F, F,
+ W, W, W, E, E, E, E, E, W, W,
+ F, F, P, E, E, E, E, E, W, W,
+};
+static const uint8_t room15[30] = {
+ W, W, W, E, E, E, E, E, W, W,
+ F, F, P, E, E, E, E, E, W, W,
+ W, W, W, E, E, E, E, E, W, W,
+};
+/* Ряд 2 комнаты 14 = «ряд −1» комнаты 15 (pop_map_set_above): после перехода
+ * зацеп продолжает целиться в ту же кромку, уже как в потолок. */
+static uint8_t above14[10];
+
+#define ACT_MIDAIR 3
+#define ACT_FREEFALL 4
+
+static uint8_t went_below; /* Kid уже пересёк нижнюю границу комнаты 14 */
+
+/* Один кадр + переход в комнату снизу — ровно то, что делает главный цикл
+ * roomtest.c по pop_fell_out (enter_room(down) + exit_room_timer). */
+static void step14(uint8_t keys)
+{
+ sc_trace_clear(); /* трасса тут не нужна, важен исход */
+ sc_run(keys, 1);
+ if (pop_fell_out) {
+ uint8_t i;
+ pop_fell_out = 0;
+ went_below = 1;
+ for (i = 0; i < 10; i++) above14[i] = room14[20 + i];
+ sc_room(room15, 15);
+ pop_map_set_edges(0, 0, 14, 0); /* сверху — комната 14 */
+ pop_map_set_above(above14);
+ pop_leave_timer = 2;
+ }
+}
+
+/* Исход попытки:
+ * 'G' — повис на НИЖНЕЙ кромке (то, ради чего тест: ряд 2 комнаты 14);
+ * 'U' — зацепился обратно за ту кромку, с которой сорвался (ряд 0) —
+ * это другой, всегда работавший случай, и засчитывать его нельзя;
+ * '.' — пролетел мимо; 'x' — так и не сорвался (проба не состоялась). */
+static char probe_down(uint8_t x, uint8_t delay)
+{
+ uint8_t i;
+ sc_room(room14, 14);
+ pop_map_set_edges(0, 0, 0, 15); /* links комнаты 14: снизу — 15 */
+ pop_map_set_above(0);
+ pop_leave_timer = 0;
+ pop_fell_out = 0;
+ went_below = 0;
+ /* Лицом ВЛЕВО у правой кромки кнопки: обрыв позади — это и есть поза
+ * спуска (pop_down_action, seg005:482). */
+ sc_kid_at_x(2, 0, x, -1);
+
+ /* ↓ + Shift — спуск с уступа (pop_down_action, seq_68) и вис на кромке.
+ * Shift держим, иначе Кид сорвётся, не успев повиснуть. */
+ for (i = 0; i < 30; i++) {
+ step14(SC_D | SC_SHIFT);
+ if (Kid.action == ACT_HANG_CLIMB || Kid.action == ACT_HANG_STRAIGHT) break;
+ }
+ if (Kid.action != ACT_HANG_CLIMB && Kid.action != ACT_HANG_STRAIGHT) return 'x';
+ /* отпустить Shift — сорваться с виса (hang_fall, seg005) */
+ for (i = 0; i < 12; i++) {
+ step14(0);
+ if (Kid.action == ACT_MIDAIR || Kid.action == ACT_FREEFALL) break;
+ }
+ if (Kid.action != ACT_MIDAIR && Kid.action != ACT_FREEFALL) return 'x';
+
+ for (i = 0; i < delay; i++) step14(0); /* пауза перед Shift */
+ for (i = 0; i < 30; i++) {
+ step14(SC_SHIFT);
+ if (Kid.action == ACT_HANG_CLIMB || Kid.action == ACT_HANG_STRAIGHT) {
+ if (!went_below) return 'U';
+ /* За ЧТО взялся: после перехода вниз ряд 0 = кромка ряда 2
+ * комнаты 14 (наша цель), ряд 2 = кромка ряда 1 комнаты 15
+ * (этажом ниже — этот случай работал и раньше). */
+ return Kid.curr_row == 0 ? 'G' : 'g';
+ }
+ }
+ return '.';
+}
+
+#define XD_LO 90
+#define XD_HI 113
+
+#define XD_DELAY_MAX 8
+
+TC_TEST(grab_below_room_edge_window_exists)
+{
+ uint8_t x, d, n = 0, n0 = 0;
+ for (x = XD_LO; x <= XD_HI; x++)
+ for (d = 0; d <= XD_DELAY_MAX; d++)
+ if (probe_down(x, d) == 'G') { n++; if (!d) n0++; }
+ TC_TRUE(n > 0);
+ /* И играбельность: зацеп обязан удаваться при самом естественном вводе —
+ * отпустил вис и сразу зажал Shift (d = 0). Окно, доступное только с
+ * точной задержкой, для игрока почти равно отсутствующему. */
+ TC_TRUE(n0 > 0);
+ if (!n) { /* карта исходов: строка = задержка Shift */
+ uint8_t i;
+ for (i = 0; i <= XD_DELAY_MAX; i++) {
+ put('d'); put((char)('0' + i)); put(':'); put(' ');
+ for (x = XD_LO; x <= XD_HI; x++) put(probe_down(x, i));
+ put('\n');
+ }
+ }
+}
+
int main(void)
{
sc_init();
TC_RUN(grab_window_exists);
TC_RUN(grab_window_reachable_by_safe_step);
TC_RUN(grab_same_for_all_key_patterns);
+ TC_RUN(grab_below_room_edge_window_exists);
return 0;
}