Files
Sprinter-SDCC/applications/PoP/roomtest/BUGS_OPEN.md
T
snark13 5348feb5f4 Доски приведены в соответствие с кодом; bug_list/bug_closed → BUGS_OPEN/BUGS_CLOSED
Доска отставала от кода на три задачи — планировать по ней было нельзя.
Сверка проведена грепом по исходникам, а не по записям:

- 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>
2026-08-11 16:10:10 +03:00

374 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
**Здесь ТОЛЬКО незакрытое.** Всё закрытое (и, что важнее, разбор корней)
живёт в [`BUGS_CLOSED.md`](BUGS_CLOSED.md) — прежде чем заводить новый баг,
грепни там по симптому. Сырые формулировки пользователя с прогонов —
[`bugs_level1.md`](bugs_level1.md) / [`bugs_level2.md`](bugs_level2.md).
Приоритеты работ — в [`TASKS_OPEN.md`](TASKS_OPEN.md) (закрытые задачи с
протоколами — [`TASKS_CLOSED.md`](TASKS_CLOSED.md)), а не здесь. Правило
проекта: механику сверять с `../SDLPoP/src/` ДО кодинга.
**Ревизия 2026-08-11:** файл вычищен от закрытых записей (правило «в `_OPEN`
только открытое»). Уровни 1-4 приняты smoke-тестами; крупных багов нет.
| ID | что | тип | статус |
|----|-----|-----|--------|
| [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить |
| [MIRROR-FG-STALE](#mirror-fg-stale) | зеркало поставлено, пока игрок В комнате 4 → коллизия его не видит | **низкий** (в обычном прохождении недостижимо) | открыт, фикс на несколько строк |
| [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт |
| [TORCH-ANIM-RIGHT](#torch-anim-right) | под запечённым пламенем застывают не только челюсти чомпера | **низкий** | открыт: на уровнях 1-4 такого соседства нет |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
---
<a id="уровень-2"></a>
# С приёмки уровня 2 (2026-08-07)
Всё найденное тем прогоном закрыто, кроме двух записей ниже
(`BUG-SPIKE-1` — уровень 2, комната 6). Карта содержимого уровня 2 (что где
стоит по `res2002.bin`, какие кнопки какие ворота открывают) — в
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#l2-pass), по ней видно, «механика не
сработала» это или «так и задумано».
<a id="bug-spike-1"></a>
## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
> **Статус 2026-08-05, вечер.** Пользователь **повторить не смог**, а замер
> (ниже) показал, что чистый пробег по убранным пикам убивает штатно. То
> есть в смертельности бага, похоже, нет вовсе: наблюдался частный случай —
> пики, УЖЕ выдвинутые полностью (h = 1), для бегущего безвредны и в
> оригинале. Запись оставлена открытой ровно из-за визуального расхождения
> со скриншотом SDLPoP (у нас острия торчат, у него убраны) — см. конец.
>
> **Как получить состояние нарочно:** подойти к пикам вплотную (выдвинутся),
> отступить на полшага, НЕ выходя из зоны срабатывания, и пробежать по ним.
> Пользователь пробовал и это, и попиксельную подгонку X читом `[`/`]` —
> не поднялось. Вывод для будущего разбора: состояние **не чисто
> позиционное**, одной шириной габарита его не объяснить; следующий
> подозреваемый — момент, в который `process_trobs` застаёт модификатор
> относительно кадра Кида.
**Наблюдение (пользователь, 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 построчно и совпадают дословно.
**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом).** Кид уронил
плиту-потолок и спрыгнул вниз; пики выдвинулись и «спрятались не все — часть
артефактов осталась». Скриншоты рядом:
[наш](bugscreens/l2-r6-spikes-ours.png) и
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под
щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.
**ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер
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="уровень-3"></a>
---
# Оптимизация отрисовки (записано 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>
---
<a id="bug-chomp-jump-1"></a>
## BUG-CHOMP-JUMP-1. Прыжок с места вплотную к чомперу: кадр с отступом назад — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
> **Статус 2026-08-08.** Наблюдение пользователя на приёмке L3-CHOMP: Кид
> стоит вплотную СЛЕВА от чомпера лицом вправо, прыжок с места — в анимации
> проскакивает кадр, где он «чуть отступил назад», и только потом идёт
> прыжок. В оригинале прыжок идёт прямо с места. **На повторе в тот же
> заход не воспроизвёлся** — отложено до пререлиза.
**Что уже установлено (чтобы не разбирать заново).**
1. **Это НЕ анимация.** `seq_3_standing_jump` (SDLPoP `seqtbl.c:382`)
состоит только из положительных смещений:
`act(run_jump), f16, f17, dx(2) f18…f22, dx(7) f23, dx(9) f24, dx(5) dy(-6) f25`.
Ни одного отрицательного `dx` — отката в последовательности нет вовсе.
Значит отступ даёт `bumped()`, то есть физика посчитала въезд в
препятствие и выровняла `Char.x` назад.
2. **`is_obstacle` для чомпера у нас совпадает с оригиналом** (seg004:037E):
препятствие только при `modif == 2`, причём именно `== 2`, БЕЗ маски
`0x7F` — то есть окровавленный чомпер (`0x82`) в ванили не бампит вовсе.
Проверено, расхождения нет.
3. **Прямая трасса прыжка отката НЕ показала.** Ввод «UP и RIGHT
одновременно» (mame bridge, `key UP 24` + `key RIGHT 24`) даёт чистое
движение вперёд: `x` 148 -> 155, кадр становится 178 (перемололо). Ни
одного кадра с уменьшением `x`.
**Главная гипотеза — ПОРЯДОК НАЖАТИЙ.** Если ↑ приходит на кадр раньше →,
то `control_standing` уходит не в `standing_jump()`, а в `up_pressed()`
вертикальный прыжок с зацепом, а он **выравнивает `Char.x`**. Отсюда и
«отступил, потом прыгнул». Проверять надо `check_jump_up` /
`jump_up_or_grab` / `grab_up_no_floor_behind` (seg005:0836 и далее), а не
прыжок с места. Внимание на `can_climb_up` (seg005:0828): там у чомпера
ЕСТЬ спецкейс (`seq_73_climb_up_to_closed_gate` при взгляде ВПРАВО) — он у
нас портирован (`pop_map.c`, ветка `TILE_MIRROR || TILE_CHOMPER`), но именно
вокруг него и стоит копать.
**Как ловить.** Брейкпоинт на резидентном `_kid_tick` с
`{printf "f=%d x=%d act=%d col=%d",b@Kid+0,b@Kid+1,b@Kid+6,b@Kid+4; g}`
одна строка на кадр, адрес `_Kid` из `.sprinter-cc-roomtest/roomtest.map`
(после КАЖДОЙ пересборки другой). Плюс стоп-кадр `1` в момент отступа и
чтение `Kid` из памяти. Метод — memory `z80_profiling_method`.
---
## Заметки (отладка)
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
сторону = разворот (как в оригинале safe_step), движение со второго.
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
**S** — выдать меч, **Shift+L** — следующий уровень, **`[`/`]`** — сдвиг
Кида на пиксель по X.
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в
настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
Известный баг этого чита закрыт — [BUG-CHEAT-FIGHT-1](BUGS_CLOSED.md#bug-cheat-fight-1).
- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство
данных уровня (разбор — «НЕ БАГИ» в [`BUGS_CLOSED.md`](BUGS_CLOSED.md));
приоритет багов в них низкий. Аналогично 23/24 на уровне 3.
---
<a id="died-on-button"></a>
## DIED-ON-BUTTON. `died_on_button` (seg007:776) не портирован
Обнаружено при разборе [BUG-LOOSE-BUTTON-1](BUGS_CLOSED.md#bug-loose-button-1).
`check_press` (seg006:1707) разбирает ЛЮБОГО мёртвого `Char`, не только Кида:
```c
if (curr_tile2 == tiles_15_opener || curr_tile2 == tiles_6_closer) {
if (Char.alive < 0) trigger_button(1, 0, -1); /* жив */
else died_on_button(); /* мёртв */
}
```
`died_on_button` на **opener** делает тайл обычным полом и форсирует
`button_type = tiles_14_debris` — ворота открываются насовсем; на любой другой
кнопке ставит `tiles_5_stuck` (заклинена, `link_timer == 0x1F`, связь мертва).
У Кида эффект живёт до перезапуска уровня (смерть → `is_restart_level`
`play_level` заново зовёт `load_level()`, тайлы перечитываются), а вот когда на
кнопке умирает СТРАЖ — перезапуска нет, и кнопка заклинена до конца уровня.
Что нужно: сам порт `died_on_button`, константа `TILE_STUCK = 5` и тайл
заклиненной кнопки в атласе фона (сейчас его там нет).
---
<a id="torch-anim-right"></a>
## TORCH-ANIM-RIGHT. Под пламенем могут застыть не только челюсти чомпера
Открыто 2026-08-10 при закрытии
[BUG-TORCH-CHOMP-2](BUGS_CLOSED.md).
Пламя факела запекается в фон и рисуется в ячейке ПРАВОГО СОСЕДА. Оригинал
после каждого кадра факела метит этого соседа (`set_redraw_anim_right`,
seg007:0101) и перерисовывает весь его слой `anim` поверх огня; мы метим
только когда сосед — чомпер. Слой `draw_tile_anim` (seg008:0644) рисует
также **пики, зелье и меч** — если такой тайл окажется справа от факела и
будет в статике, пламя накроет и его.
На уровнях 1–4 такого соседства не встретилось, поэтому расширять пометку
(она стоит перерисовки тайла каждый кадр на каждый факел) заранее не стали.
**Как проверять:** поставить факел слева от пики/меча/зелья в тестовой
комнате и дождаться статики; либо пройти уровни 5+ и смотреть на клетку
справа от каждого факела.
**Как чинить, если встретится:** там же, в ветке факела `pop_process_trobs`,
добавить в `trob_rcode`-проверку нужные коды и соответствующий вид пометки
(`POP_RD_SPIKE` / `POP_RD_FLOOR`), а зелье — переставить в порядке обхода
так, чтобы оно рисовалось ПОСЛЕ факела.
---
<a id="fore-dup"></a>
## FORE-DUP. Передний слой тайла рисуется ДВАЖДЫ при перекрытии объектов
Найдено пользователем 2026-08-10 (вопросом, а не по симптому — картинка
верная, страдают только такты).
**У нас.** Fore-проход зовёт КАЖДЫЙ рисующий по своему прямоугольнику:
`pop_char_fore` для слотов Кида и соперника, `pop_mirror_draw` для отражения.
Окно клипа (`pop_fore_set_clip`) у каждого своё. Если футпринты двух
объектов накрывают один тайл, его передний кусок рисуется два раза.
Когда случается:
- **Кид и отражение** — ВСЕГДА (стоят на одном тайле зеркала); этот случай
создан фиксом fore-прохода над отражением 2026-08-10;
- **Кид и соперник в одном тайле** — то есть весь ближний бой;
- Кид и падающий кусок плиты.
**Картинку не портит:** куски переднего слоя идут прозрачным блитом-копией,
операция идемпотентная. Портило бы при XOR/mono с накоплением — таких в
fore-слое нет.
**Такты тратит,** и в самом дорогом месте: fore-проход исторически самая
тяжёлая часть кадра (memory `pop_fore_layer_cost` — было 78 % кадра, окно
клипа и кэш кладки дали 3.2×).
**Как устроено в оригинале — дубль НЕВОЗМОЖЕН по построению.**
`redraw_at_char` (seg003:0427) ничего не рисует, а только помечает тайлы
своего прямоугольника:
```c
for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row)
for (tile_col = x_col_left; tile_col <= x_col_right; ++tile_col)
set_redraw_fore(get_tilepos(tile_col, tile_row), 1);
```
а `set_redraw_fore` (seg007:0550) — это `redraw_frames_fore[tilepos] = frames`,
ПРИСВАИВАНИЕ. Два персонажа на одном тайле оставят там ту же единицу, и
единственный обход тайлов нарисует передний кусок один раз.
**Что делать.** Механизм пометок у нас уже есть и прямо назван портом этой
архитектуры — `pop_redraw.h`. Fore-слой остался единственным местом на
прямом вызове. Приведение к оригиналу: `pop_char_fore`/`pop_mirror_draw`
вместо прохода ставят пометки, а один проход в конце кадра их разбирает.
**СНАЧАЛА ЗАМЕРИТЬ, потом чинить.** Это самый горячий путь, и окно клипа
(вместо перебора тайлов) в своё время дало 3.2× — переход на пометки может
часть этого вернуть назад. Замер: брейкпоинт на листьях fore-слоя со
счётчиком, сцена «бой в комнате 3 уровня 1» и «Кид на тайле зеркала,
ур. 4»; сравнить число нарисованных кусков с числом уникальных тайлов.
Если дубль мал — оставить как есть и закрыть запись.
---
<a id="mirror-fg-stale"></a>
## MIRROR-FG-STALE. Зеркало, поставленное при игроке В комнате 4, не попадает в коллизию
Заведено 2026-08-11 как краевой случай, оставшийся открытым от
[L4-MIRROR](TASKS_CLOSED.md#l4-mirror) (шаг 2). **Низкий приоритет: в
обычном прохождении недостижимо** — дверь выхода уровня 4 стоит не в той
комнате, где зеркало, поэтому игрок физически не может быть в комнате 4 в
момент её открытия.
**Причина (по коду, не по симптому).** `place_mirror` (`pop_trob.c`) пишет
тайл зеркала в ДАННЫЕ УРОВНЯ (`pop_level_set_tile` — страница уровня в W0) и,
если комната зеркала на экране, помечает тайл на перерисовку. А коллизия
работает не с данными уровня, а со СНИМКОМ комнаты `room_fg`, который
`pop_room_load` делает один раз при входе в комнату (`pop_map_set(room_fg)`
в `roomtest.c`). Снимок в этот момент не обновляется.
**Что будет:** зеркало нарисуется (перерисовка тайла отработает), но для
коллизии его как бы нет — `wall_type` не вернёт «стена слева», а ветка
`is_obstacle` для прыжка сквозь зеркало не сработает. То есть Кид пробежит
сквозь зеркало насквозь и **тень не родится**, пока комнату не перезайти.
**Сценарий проверки** (нужен чит `+`/`` ROOMNAV, иначе не собрать):
1. уровень 4, нажать кнопку, открывающую дверь выхода;
2. **пока дверь анимируется** (43 тика ≈ 3.5 с) уйти читом в комнату 4;
3. дождаться, когда дверь дорисует открытие — зеркало появится на экране;
4. разбежаться справа налево и прыгнуть в зеркало.
- Ожидание при баге: Кид пролетает насквозь как через пустоту, тени нет.
- После выхода из комнаты и возврата в неё всё работает нормально.
**Фикс — несколько строк:** в `place_mirror`, в ветке
`cur_room == MIRROR_ROOM`, обновить и живую карту, а не только данные уровня
`pop_map.c` уже есть внутренние точки записи `g_fg[tilepos] = ...`,
нужна публичная «поставить тайл в текущей комнате»). Делать вместе с любой
следующей правкой `pop_trob.c` — отдельного захода не стоит.