diff --git a/applications/PoP/docs/perf_backlog.md b/applications/PoP/docs/perf_backlog.md
index c309974..727e9f2 100644
--- a/applications/PoP/docs/perf_backlog.md
+++ b/applications/PoP/docs/perf_backlog.md
@@ -69,6 +69,34 @@ MAME (`z80_profiling_method`) на роомтесте, уровень 1 комн
139 398 (27 %), циан (`PROF(6)`) 233 515 (45 % работы = 54 % растрового
кадра, начинается на 74 % первого кадра и кончается на 123 %).
+## СДЕЛАНО 2026-08-10: метка «фон трогали» стала маской ТАЙЛОВ
+
+Было: один union-прямоугольник на страницу. Три факела трогают по пятну
+16x18 в колонках 1, 6 и 8, а их объединение — полоса `x 40..280` на всю
+комнату; Кид, стоящий где угодно между крайними факелами, в неё попадал и
+перерисовывался каждый кадр со всем fore-проходом.
+
+Стало: `uint16_t pop_cd_dmask[2][3]` — бит на колонку, слово на ряд, набор на
+страницу. Колонка берётся сдвигом (`x >> 5`), ряд — цепочкой сравнений;
+проверка в `cd_quiet` — три `AND` через резидентный `pop_cd_hit`.
+
+Гранулярность тайла — это гранулярность ОРИГИНАЛА: пометки там тоже по
+тайлам (`redraw_frames_anim[tilepos]`, `set_wipe`), персонажи привязаны к
+тайлу через `tile_object_redraw[tilepos]`, а единственное подтайловое
+уточнение (`wipe_heights`) — по высоте, не по ширине. Полутайл (16 px) не дал
+бы ничего: пламя рисуется с отступом 8 px и шириной 16, то есть занимает
+середину тайла и задевает обе половины.
+
+Замер на той же сцене, где снимался профиль ниже (Кид неподвижно на (1,7)):
+
+| | было | стало |
+|---|---:|---:|
+| циан (спрайты + fore) | 233 515 | **24 781** |
+| работа за кадр | 517 609 | **306 553** |
+| период | 4 растровых кадра | **3** |
+
+---
+
## 0. Дворцовая кладка в fore-проходе — 27 % кадра НА НОЛЬ ПИКСЕЛЕЙ
**Цена: 139 863 такта за кадр (32 % растрового кадра), полезных пикселей —
diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md
index cd44d6e..4aa5305 100644
--- a/applications/PoP/roomtest/bug_closed.md
+++ b/applications/PoP/roomtest/bug_closed.md
@@ -13,6 +13,96 @@
---
+
+## BUG-LOOSE-BUTTON-1. Упавшая плита не нажимает кнопку — **ЗАКРЫТ 2026-08-10**
+
+**Симптом (пользователь, уровень 4).** Плита `16(1,1)` падает вниз, в комнате
+17 ничего не меняется: кнопка цела, щебня нет, ворота не открылись.
+
+**Корень — три слоя.**
+
+1. Порт `loose_land` (seg007:11E8) у нас вообще не звал `trigger_button`: и в
+ своей комнате (`pop_map.c`), и в комнате снизу (`roomtest.c`) он только
+ клал щебень.
+2. `pop_room_col_landing` (pop_level.c) считала площадкой **только чистый
+ пол** (`t == 1`). У оригинала площадок семь — `tiles_1_floor`,
+ `tiles_2_spike`, `tiles_6_closer`, `tiles_10_potion`, `tiles_15_opener`,
+ `tiles_19_torch`, `tiles_30_torch_with_debris`; на всём прочем кусок просто
+ исчезает. Кнопка в набор не входила, поэтому плита растворялась.
+3. Сигнал `pop_loose_fell` взводится в момент ОТРЫВА плиты, а по нему главный
+ цикл и щебень клал, и кнопку жал — ворота начинали открываться, пока плита
+ ещё в воздухе.
+
+**Механика оригинала (важно, интуиция обманывает).** Плита не «постоянно
+давит» на кнопку. Нажатие ОДНО, но с `button_type = tiles_14_debris`, а это у
+`trigger_gate` отдельная ветка: возврат 2 → `animate_door` доводит створку и
+ставит `curr_modifier = 0xFF` → ворота заморожены открытыми навсегда, обычный
+opener их больше не трогает (`if (modifier == 0xFF) return -1`). Сама кнопка
+съедается: тайл становится щебнем. На closer — быстрое закрытие и тоже
+щебень. На факеле остаётся отдельный тайл `tiles_30_torch_with_debris`.
+
+**Фикс.** Набор площадок расширен до оригинального; `pop_trigger_button`
+зовётся в обоих местах с правильным `button_type`; посадка в комнате снизу
+переехала с `pop_loose_fell` на новый сигнал `pop_loose_exit`, который
+`mob_tick_one` взводит, когда кусок ушёл ниже поля комнаты.
+
+**Осознанное расхождение.** Оригинал через `mob_down_a_row` продолжает
+симуляцию куска уже в нижней комнате; у нас он там не летит, а садится сразу.
+Задержка получается «высота комнаты», а не «высота комнаты плюс путь до пола
+внизу».
+
+---
+
+
+## BUG-GATE-FF-1. Ворота, открытые навсегда, нарисованы открытыми, но непроходимы — **ЗАКРЫТ 2026-08-10**
+
+**Симптом.** После BUG-LOOSE-BUTTON-1 ворота `23(0,9)` рисуются поднятыми, но
+Кид в них упирается.
+
+**Корень — перегруженное значение.** `gate_modif` (pop_map.c) отдаёт `0xFF`
+как сторожевое «тайла нет за краем уровня», а `trigger_gate` ставит тот же
+`0xFF` как живой модификатор «открыто НАСОВСЕМ». В `gate_passable` стояло
+`if (gm == 0xFF) return 0;`. До сегодняшнего дня значение в игре не
+возникало — ветку по щебню никто не запускал.
+
+**Фикс.** Ветка убрана: она мёртвая (нас зовут только когда тайл ТОЧНО
+ворота), а `(0xFF >> 2) + 6 = 69` и так больше любого `fph`. Остальные
+читатели `gate_modif` трактуют 63 как «открыто» и правок не потребовали.
+
+---
+
+
+## BUG-TORCH-CHOMP-1. Чомпер стирает пламя соседнего факела — **ЗАКРЫТ 2026-08-10**
+
+**Симптом (пользователь).** Комната 23 уровня 4: факел есть, огня нет.
+
+**Корень.** Пламя рисуется в ячейке ПРАВОГО соседа (`x = COL_XH[col+1]*8 + 8`
+= 232..248, `y` 33..50), то есть в тайле чомпера (0,7). `pop_chomp_redraw`
+начинается с `pop_heal_off(224, 2, 32, 64)` — восстанавливает запечённый фон в
+`x 224..255, y 30..93`. Пламя в фон не запекалось и рисовалось в
+`GFX_BANK_SPRITE`, поэтому heal его съедал; вдобавок `pop_redraw_needed` идёт
+ПОСЛЕ `pop_process_trobs`, так что стиралось только что нарисованное.
+
+**Тупик, в который я сначала свернул.** Вынес отрисовку пламени в отдельный
+проход после `pop_redraw_needed` — стало хуже: пламя легло ПОВЕРХ челюстей
+(правильный z-порядок существует только внутри общего обхода тайлов, где
+`draw_tile_anim_right` идёт до `draw_tile_anim`), плюс проход индексировал
+`trob_code[i]` параллельно `trobs[i]`, а `pop_process_trobs` в конце уплотняет
+массив — индексы разъезжались, и чужой trob рисовался как факел (пламя из
+головы Кида). Проход откачен.
+
+**Фикс (подсказан пользователем).** Пламя ЗАПЕКАЕТСЯ: `pop_torch_draw` пишет
+в `GFX_BANK_NORMAL` (видео-ОЗУ + ОЗУ-копия). Это безопасно именно у факела —
+все девять кадров лежат на общем канвасе 16×18 и упакованы `opaque=True`, то
+есть кадр полностью накрывает предыдущий, протухнуть в копии нечему; и факел
+всегда задний план. Тогда heal чомпера возвращает огонь сам, а челюсти
+ложатся поверх — тот же z-порядок, что в оригинале, и без лишнего прохода.
+
+Пузырёк ЗЕЛЬЯ так нельзя: он ползёт вверх, себя не накрывает и требует heal —
+`pop_potion_draw` остаётся в `GFX_BANK_SPRITE`. Столкнуться с чомпером в
+одном тайле зелье не может.
+
+
## BUG-GUARD-IX-1. «Зависание» при бое со стражем — затёртый IX главного цикла — **ЗАКРЫТ 2026-08-10**
diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md
index 2478a1e..6b552b8 100644
--- a/applications/PoP/roomtest/bug_list.md
+++ b/applications/PoP/roomtest/bug_list.md
@@ -25,6 +25,10 @@
| ~~BUG-GATE-PASS-1~~ | проход сквозь закрытую решётку шва | Major | **ЗАКРЫТ 2026-08-09** — [bug_closed.md](bug_closed.md#bug-gate-pass-1), смоук уровня 1 пройден |
| ~~BUG-GUARD-IX-1~~ | «зависание» в бою со стражем: затёрт IX главного цикла | Blocker | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-guard-ix-1) |
| ~~BUG-GATE-SEAM-ROW1~~ | решётка в шве не анимируется, если ворота не в ряду 0 | Major | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-gate-seam-row1) |
+| ~~BUG-LOOSE-BUTTON-1~~ | упавшая плита не нажимает кнопку под собой | Major | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-loose-button-1) |
+| ~~BUG-GATE-FF-1~~ | ворота «открыты навсегда» непроходимы (0xFF перегружен) | Major | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-gate-ff-1) |
+| ~~BUG-TORCH-CHOMP-1~~ | чомпер стирает пламя соседнего факела | Minor | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-torch-chomp-1) |
+| [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт |
---
@@ -310,3 +314,30 @@ Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал
- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство
данных уровня (разбор — «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md));
приоритет багов в них низкий. Аналогично 23/24 на уровне 3.
+
+
+---
+
+
+## DIED-ON-BUTTON. `died_on_button` (seg007:776) не портирован
+
+Обнаружено при разборе [BUG-LOOSE-BUTTON-1](bug_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` и тайл
+заклиненной кнопки в атласе фона (сейчас его там нет).
diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c
index ba54fcd..4be8d7d 100644
--- a/applications/PoP/roomtest/pop_cdraw.c
+++ b/applications/PoP/roomtest/pop_cdraw.c
@@ -235,8 +235,8 @@ static uint8_t cd_quiet(uint8_t who, uint8_t p)
int x0, y0, x1, y1;
cd_bbox(who, p, &x0, &y0, &x1, &y1);
y0 += POP_YOFF; y1 += POP_YOFF; /* метка фона — в ЭКРАННЫХ */
- if (x0 < pop_cd_dx1[p] && pop_cd_dx0[p] < x1 &&
- y0 < pop_cd_dy1[p] && pop_cd_dy0[p] < y1) return 0;
+ /* cd_bbox отдаёт x1/y1 ИСКЛЮЧИТЕЛЬНО, pop_cd_hit ждёт включительно. */
+ if (pop_cd_hit(p, x0, y0, x1 - 1, y1 - 1)) return 0;
}
cd_sig_make(who, &g);
a = (const uint8_t *)&g; b = (const uint8_t *)&cd_sig[who][p];
diff --git a/applications/PoP/roomtest/pop_cdraw.h b/applications/PoP/roomtest/pop_cdraw.h
index 166cd3c..19d57ec 100644
--- a/applications/PoP/roomtest/pop_cdraw.h
+++ b/applications/PoP/roomtest/pop_cdraw.h
@@ -120,8 +120,12 @@ void pop_char_set_render_dx(uint8_t who, int dx) __banked;
*
* Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья,
* а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */
-extern uint8_t pop_cd_dirty; /* биты страниц: прямоугольник взведён */
-extern int pop_cd_dx0[2], pop_cd_dy0[2], pop_cd_dx1[2], pop_cd_dy1[2];
+extern uint8_t pop_cd_dirty; /* биты страниц: метка взведена */
+extern uint16_t pop_cd_dmask[2][3]; /* [страница][ряд] = биты колонок 0..9 */
+/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
+ * что трогали на странице p. Резидент (pop_tile.c). */
+uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1);
+void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */
void pop_cd_touch(int x, int y, int w, int h);
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c
index 39788bc..088a18d 100644
--- a/applications/PoP/roomtest/pop_level.c
+++ b/applications/PoP/roomtest/pop_level.c
@@ -243,7 +243,14 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col)
for (row = 0; row < 3; row++) { /* кусок пролетает пустые ряды */
uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
if (t == 0) continue; /* empty — дальше вниз */
- if (t == 1) res = row; /* floor — приземление → debris */
+ /* Набор площадок — дословно switch из loose_land (seg007:11E8):
+ * пол, пика, обе кнопки, зелье и оба факела. На всём остальном
+ * (стена, ворота, колонна) кусок просто исчезает. Раньше здесь
+ * стоял только пол, и плита, падавшая на КНОПКУ в комнате снизу,
+ * пропадала бесследно: ни щебня, ни нажатия (уровень 4, плита
+ * 16(1,1) -> кнопка 17(0,1), открывающая ворота 23(0,9)). */
+ if (t == 1 || t == 2 || t == 6 || t == 10 ||
+ t == 15 || t == 19 || t == 30) res = row;
break; /* первый не-пустой тайл — стоп */
}
gfx_w0_unmap();
diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c
index ea81a85..2e2d356 100644
--- a/applications/PoP/roomtest/pop_map.c
+++ b/applications/PoP/roomtest/pop_map.c
@@ -54,6 +54,8 @@
#define TILE_WALL 20
#define TILE_LOOSE 11 /* проваливающийся пол */
#define TILE_DEBRIS 14 /* щебень (упавший loose) */
+#define TILE_TORCH 19 /* факел */
+#define TILE_TORCH_DEBRIS 30 /* факел + щебень (loose_land) */
#define TILE_SPIKE 2 /* пики */
#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */
#define TILE_POTION 10 /* зелье */
@@ -319,7 +321,14 @@ uint8_t pop_gate_modif(int8_t col, int8_t row) __banked
static uint8_t gate_passable(int8_t col, int8_t row)
{
uint8_t gm = gate_modif(col, row);
- if (gm == 0xFF) return 0;
+ /* Ветки `if (gm == 0xFF) return 0;` здесь БЫТЬ НЕ ДОЛЖНО: 0xFF — это не
+ * только сторожевое «тайла нет» из gate_modif, но и живой модификатор
+ * «ворота открыты НАСОВСЕМ» (trigger_gate по button_type = щебень,
+ * seg007:623). Значение это появилось только когда заработал порт
+ * loose_land, и ворота выходили нарисованными открытыми (отрисовка
+ * клампит modl>188 -> 188), но непроходимыми. Сторож сюда не приходит:
+ * зовут нас только там, где тайл ТОЧНО ворота, а (0xFF>>2)+6 = 69 и так
+ * больше любого fph. */
return (uint8_t)((gm >> 2) + 6) >= (uint8_t)CD_ACT->fph;
}
@@ -2126,9 +2135,24 @@ void pop_loose_tick(void) __banked
if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */
uint8_t pos = (uint8_t)(pop_loose_landed - 1);
uint8_t r = 0, c = pos;
+ uint8_t tt = (uint8_t)(g_fg[pos] & 0x1F); /* тайл, НА который села плита */
pop_loose_landed = 0;
while (c >= 10) { c -= 10; r++; }
- g_fg[pos] = TILE_DEBRIS;
+ /* Плита, севшая на КНОПКУ, её нажимает (seg007:11E8, ветки
+ * tiles_15_opener / tiles_6_closer). Тонкость: на opener оригинал
+ * подставляет button_type = tiles_14_debris, а это у trigger_gate
+ * отдельная ветка «открыть НАСОВСЕМ» (возврат 2 -> animate_door
+ * доводит створку и ставит modifier 0xFF, после чего ворота
+ * заморожены и обычный opener их больше не трогает). Сама кнопка
+ * съедается: тайл станет щебнем ниже, нажаться второй раз нечему.
+ * Модификатор (индекс LINKLOC) тайл не меняет, берём его как есть. */
+ if (tt == TILE_OPENER || tt == TILE_CLOSER)
+ pop_trigger_button(g_room, pos,
+ (uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
+ pop_trob_modif(g_room)[pos]);
+ /* На факеле — отдельный тайл «факел с щебнем» (seg007:1067). */
+ g_fg[pos] = (uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
+ ? TILE_TORCH_DEBRIS : TILE_DEBRIS);
/* Перерисовка ПО ПЛОЩАДИ ПОЛА (не LOOSE_GONE: та стирает и верх стены
* ряда ниже — оставался чёрный прямоугольник под тайлом). Обе
* страницы дабл-буфера. */
diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c
index e57421c..b54e622 100644
--- a/applications/PoP/roomtest/pop_room.c
+++ b/applications/PoP/roomtest/pop_room.c
@@ -355,9 +355,11 @@ static void draw_tile(int row, int col)
} else if (lcode == 16) { /* дверь уровня */
draw_leveldoor(lmod, xh, dmy);
}
- /* Пламя факела (seg008:560, chtab_1 id modifier+1) в фон НЕ запекаем:
- * оно анимируется каждый кадр, а heal вернул бы запечённый кадр из-под
- * нового. Рисует pop_torch_draw (динамика, pop_process_trobs). */
+ /* Пламя факела (seg008:560, chtab_1 id modifier+1) здесь, в статичной
+ * отрисовке тайла, не рисуем — его ведёт pop_torch_draw из
+ * pop_process_trobs каждый кадр. Но ЗАПЕКАЕТСЯ оно всё равно (там
+ * GFX_BANK_NORMAL): факел — чистый задний план, а самонакрытие кадров
+ * делает запекание безопасным. Подробности — в шапке pop_torch_draw. */
}
/* --- draw_tile_bottom (seg008.c:570) --- */
@@ -608,8 +610,22 @@ void pop_torch_draw(int row, int col, uint8_t modif) __banked
/* heal НЕ нужен: оригинал рисует пламя blitters_0_no_transp (seg008
* draw_tile_anim_right), и все 9 кадров лежат на общем канвасе 16x18 —
* непрозрачный блит сам полностью накрывает предыдущий кадр. Кадры
- * упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS). */
- gfx_set_bank(GFX_BANK_SPRITE);
+ * упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS).
+ *
+ * И ровно поэтому пламя ЗАПЕКАЕТСЯ (GFX_BANK_NORMAL: видео-ОЗУ + ОЗУ-копия),
+ * а не рисуется в SPRITE поверх фона. Запечь его безопасно именно из-за
+ * самонакрытия — следующий кадр сотрёт предыдущий сам, «протухнуть» в
+ * копии нечему. А выигрыш принципиальный: точечные перерисовки соседних
+ * тайлов начинаются с heal из ОЗУ-копии, и незапечённое пламя они стирали
+ * — чомпер (0,7) комнаты 23 уровня 4 healит x 224..255 / y 30..93 и
+ * съедал факел (0,6), который рисуется в ячейке правого соседа. С
+ * запеканием heal возвращает огонь сам, а челюсти ложатся ПОВЕРХ него —
+ * тот же z-порядок, что у оригинала, где draw_tile_anim_right (пламя
+ * левого соседа) идёт до draw_tile_anim (сам чомпер).
+ *
+ * Пузырёк ЗЕЛЬЯ так нельзя: он ползёт вверх и себя не накрывает, ему
+ * нужен heal — pop_potion_draw остаётся в SPRITE. */
+ gfx_set_bank(GFX_BANK_NORMAL);
pop_pot_b((uint8_t)(modif + 1), x, yb);
gfx_set_bank(GFX_BANK_NORMAL);
}
@@ -889,6 +905,13 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
}
}
if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
+ /* Приземление в комнате СНИЗУ разбирает главный цикл — но ИМЕННО
+ * сейчас, когда кусок долетел, а не в момент отрыва плиты: иначе
+ * кнопка внизу срабатывала, пока плита ещё в воздухе. */
+ if (!pop_loose_exit) {
+ pop_loose_exit = (uint8_t)(m->col + 1);
+ pop_loose_exit_room = m->room;
+ }
m->active = 0; m->clean = (uint8_t)(here ? 2 : 0);
return;
}
diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c
index 14d2905..17dde1b 100644
--- a/applications/PoP/roomtest/pop_state.c
+++ b/applications/PoP/roomtest/pop_state.c
@@ -14,6 +14,15 @@
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
uint8_t pop_loose_landed;
+/* Кусок loose УШЁЛ ВНИЗ из комнаты (порт хвоста move_loose, seg007:1126:
+ * mob_down_a_row переносит его в комнату снизу; у нас симуляция там не
+ * продолжается, и приземление разбирает главный цикл). 0 = нет, иначе
+ * col+1; комната, ИЗ которой он ушёл, — в pop_loose_exit_room.
+ * Сигнал отдельный от pop_loose_fell: тот взводится в момент ОТРЫВА плиты
+ * (там верен только remove_loose), и по нему кнопка внизу нажималась,
+ * пока кусок ещё летел. */
+uint8_t pop_loose_exit;
+uint8_t pop_loose_exit_room;
uint8_t pop_droppedout;
/* leveldoor_open (seg007:456): дверь уровня доехала до верха. Ставит
* анимация двери (pop_trob, банк 6), читает спецсобытие подъёма скелета
diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h
index f664b32..c0b7d1c 100644
--- a/applications/PoP/roomtest/pop_state.h
+++ b/applications/PoP/roomtest/pop_state.h
@@ -14,6 +14,12 @@
* разбирает логика loose-полов (pop_map, W1/W2). */
extern uint8_t pop_loose_landed;
+/* Кусок loose ушёл ниже поля своей комнаты: 0 = нет, иначе col+1; комната
+ * отправления — pop_loose_exit_room. Ставит отрисовка mob (банк 7),
+ * разбирает главный цикл (посадка в комнате снизу). */
+extern uint8_t pop_loose_exit;
+extern uint8_t pop_loose_exit_room;
+
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
extern uint8_t pop_immortal;
diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c
index 6bc73aa..c84d191 100644
--- a/applications/PoP/roomtest/pop_tile.c
+++ b/applications/PoP/roomtest/pop_tile.c
@@ -158,40 +158,104 @@ uint8_t pop_loose_frame(uint8_t m)
return m;
}
-/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- */
-uint8_t pop_cd_dirty;
-int pop_cd_dx0[2], pop_cd_dy0[2], pop_cd_dx1[2], pop_cd_dy1[2];
+/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- *
+ *
+ * Метка — БИТОВАЯ МАСКА ТАЙЛОВ, по слову на ряд и по набору на страницу.
+ * Раньше здесь лежал ОДИН union-прямоугольник на страницу, и это убивало
+ * пропуск перерисовки персонажа: три факела комнаты трогают по пятнышку
+ * 16x18 в колонках 1, 6 и 8, а их объединение — полоса x 40..280 на всю
+ * комнату. Кид, стоящий где угодно между крайними факелами, попадал в неё
+ * и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829
+ * тактов против 20 071, период 4 растровых кадра против 3).
+ *
+ * Гранулярность тайла (32 x 63) вместо точного прямоугольника — осознанное
+ * огрубление: факел помечает всю свою колонку по высоте ряда. Персонаж и
+ * так занимает бОльшую часть высоты ряда, зато касания перестают
+ * склеиваться. См. docs/impl_diff.md. */
+uint8_t pop_cd_dirty;
+uint16_t pop_cd_dmask[2][3];
-/* Цикл по страницам развёрнут, а x+w / y+h посчитаны один раз. Не
- * вкусовщина: индекс-переменная заставляла SDCC считать адрес каждой из
- * восьми ячеек сложением, а `x + w` в условии и в присваивании считались
- * дважды — замер в MAME дал 6 846 тактов на вызов, а зовётся он с КАЖДОГО
- * блита фона (два факела в комнате — уже 13 700 за кадр). */
+/* CD_LOW[n] = n младших единиц: маска пробега колонок c0..c1 считается как
+ * CD_LOW[c1+1] & ~CD_LOW[c0] — два чтения таблицы вместо цикла сдвигов. */
+static const uint16_t CD_LOW[11] = {
+ 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
+ 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF
+};
+
+/* Экранный y -> ряд комнаты 0..2 с клампом (полоса кладки у потолка и низ
+ * стены ложатся на крайние ряды). Цепочкой сравнений, а не делением на 63:
+ * у SDCC z80 одно деление ~5 400 тактов (memory sdcc_z80_division_hoisting). */
+static uint8_t cd_row_of(int y)
+{
+ y -= POP_YOFF;
+ if (y < 63) return 0;
+ if (y < 126) return 1;
+ return 2;
+}
+
+/* Колонки прямоугольника [x..x1] (включительно) -> битовая маска. Тайл ровно
+ * 32 px и начинается с x=0, поэтому колонка — просто сдвиг. */
+static uint16_t cd_cols_of(int x, int x1)
+{
+ uint8_t c0, c1;
+ if (x < 0) x = 0;
+ if (x1 > 319) x1 = 319;
+ if (x1 < x) return 0; /* весь прямоугольник вне экрана */
+ c0 = (uint8_t)(x >> 5);
+ c1 = (uint8_t)(x1 >> 5);
+ return (uint16_t)(CD_LOW[c1 + 1] & ~CD_LOW[c0]);
+}
+
+/* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр),
+ * поэтому цикл развёрнут: индекс-переменная заставляла SDCC считать адрес
+ * каждой ячейки сложением. Зовётся с КАЖДОГО блита фона. */
void pop_cd_touch(int x, int y, int w, int h)
{
- int x1 = x + w, y1 = y + h;
- uint8_t d = pop_cd_dirty;
- if (!(d & 1)) {
- pop_cd_dx0[0] = x; pop_cd_dy0[0] = y;
- pop_cd_dx1[0] = x1; pop_cd_dy1[0] = y1;
- } else {
- if (x < pop_cd_dx0[0]) pop_cd_dx0[0] = x;
- if (y < pop_cd_dy0[0]) pop_cd_dy0[0] = y;
- if (x1 > pop_cd_dx1[0]) pop_cd_dx1[0] = x1;
- if (y1 > pop_cd_dy1[0]) pop_cd_dy1[0] = y1;
- }
- if (!(d & 2)) {
- pop_cd_dx0[1] = x; pop_cd_dy0[1] = y;
- pop_cd_dx1[1] = x1; pop_cd_dy1[1] = y1;
- } else {
- if (x < pop_cd_dx0[1]) pop_cd_dx0[1] = x;
- if (y < pop_cd_dy0[1]) pop_cd_dy0[1] = y;
- if (x1 > pop_cd_dx1[1]) pop_cd_dx1[1] = x1;
- if (y1 > pop_cd_dy1[1]) pop_cd_dy1[1] = y1;
+ uint16_t cols = cd_cols_of(x, x + w - 1);
+ uint8_t r0, r1;
+ if (!cols) return;
+ r0 = cd_row_of(y);
+ r1 = cd_row_of(y + h - 1);
+ pop_cd_dmask[0][r0] |= cols;
+ pop_cd_dmask[1][r0] |= cols;
+ if (r1 != r0) {
+ pop_cd_dmask[0][r1] |= cols;
+ pop_cd_dmask[1][r1] |= cols;
+ if (r1 - r0 > 1) { /* прямоугольник накрыл все три ряда */
+ pop_cd_dmask[0][1] |= cols;
+ pop_cd_dmask[1][1] |= cols;
+ }
}
pop_cd_dirty = 3;
}
+/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
+ * трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
+ * вызовом, без трамплина. */
+uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1)
+{
+ uint16_t cols;
+ uint8_t r0, r1;
+ if (!(pop_cd_dirty & (1 << p))) return 0;
+ cols = cd_cols_of(x0, x1);
+ if (!cols) return 0;
+ r0 = cd_row_of(y0);
+ r1 = cd_row_of(y1);
+ if (pop_cd_dmask[p][r0] & cols) return 1;
+ if (r1 != r0) {
+ if (pop_cd_dmask[p][r1] & cols) return 1;
+ if (r1 - r0 > 1 && (pop_cd_dmask[p][1] & cols)) return 1;
+ }
+ return 0;
+}
+
+/* Страница приведена в порядок — снять с неё метку целиком. */
+void pop_cd_clear(uint8_t p)
+{
+ pop_cd_dmask[p][0] = pop_cd_dmask[p][1] = pop_cd_dmask[p][2] = 0;
+ pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
+}
+
/* ---- Блит куска атласа ---------------------------------------------- */
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
* и SDCC заводит под них кадр IX; пока он жил в общем теле, за этот кадр
diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c
index f3f3880..be03f92 100644
--- a/applications/PoP/roomtest/pop_trob.c
+++ b/applications/PoP/roomtest/pop_trob.c
@@ -555,8 +555,14 @@ void pop_process_trobs(uint8_t cur_room) __banked
if (room == cur_room &&
(code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) {
- /* Пламя/пузырёк анимируются КАЖДЫЙ кадр и в фон не запечены —
- * рисуем поверх (сами делают heal своей области). */
+ /* Пламя ЗАПЕКАЕТСЯ в фон (GFX_BANK_NORMAL внутри pop_torch_draw),
+ * пузырёк зелья — нет, ему нужен heal. Порядок «пламя раньше
+ * перерисовок тайлов» обязателен: heal чомпера-соседа вернёт
+ * запечённый огонь из ОЗУ-копии, а его собственная графика ляжет
+ * ПОВЕРХ — ровно как в оригинале, где draw_tile_anim_right (пламя
+ * левого соседа) идёт до draw_tile_anim (сам чомпер). Выносить
+ * отрисовку ПОСЛЕ pop_redraw_needed нельзя: пламя окажется поверх
+ * челюстей. */
/* Таблицей, а не tp/10 и tp%10: у Z80 деления нет, SDCC зовёт
* __divuchar + __moduchar — на два факела это четыре вызова за
* кадр (замер MAME). Тайл всегда 0..29. */
diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c
index 591a67b..a407b9c 100644
--- a/applications/PoP/roomtest/roomtest.c
+++ b/applications/PoP/roomtest/roomtest.c
@@ -252,6 +252,10 @@ static uint8_t room_label_redraw;
* терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
* комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS 0x0E
+#define TILE_CLOSER 0x06
+#define TILE_OPENER 0x0F
+#define TILE_TORCH 0x13
+#define TILE_TORCH_DEBRIS 0x1E
#define TILE_EMPTY 0x00
#define TILE_FLOOR 0x01
@@ -815,15 +819,36 @@ int main(void)
gfx_set_draw_page(back);
}
}
- if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
+ if (pop_loose_fell) { /* remove_loose: сама плита исчезла */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
- uint8_t fcol = (uint8_t)(fpos % 10);
- uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0;
- pop_level_set_tile(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
+ pop_level_set_tile(cur_room, fpos, TILE_EMPTY);
+ }
+ if (pop_loose_exit) { /* кусок долетел до низа комнаты — посадка ниже */
+ uint8_t fcol = (uint8_t)(pop_loose_exit - 1);
+ uint8_t from = pop_loose_exit_room;
+ uint8_t down = pop_room_link(from, LINK_DOWN);
+ pop_loose_exit = 0;
if (down) {
int8_t lrow = pop_room_col_landing(down, fcol);
- if (lrow >= 0) pop_level_set_tile(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
+ if (lrow >= 0) {
+ uint8_t lpos = (uint8_t)(lrow * 10 + fcol);
+ uint8_t tt = pop_level_tile(down, lpos);
+ /* Кусок сел в комнате СНИЗУ — кнопка там нажимается так
+ * же, как в своей (loose_land, seg007:11E8): на opener
+ * подставляется button_type = щебень, то есть «открыть
+ * НАСОВСЕМ» (modifier 0xFF). Второе место того же
+ * порта: своя комната — в pop_map.c. */
+ if (tt == TILE_OPENER || tt == TILE_CLOSER)
+ pop_trigger_button(down, lpos,
+ (uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
+ pop_trob_modif(down)[lpos]);
+ /* На факеле остаётся отдельный тайл «факел с щебнем»
+ * (seg007:1067), а не обычный щебень. */
+ pop_level_set_tile(down, lpos,
+ (uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
+ ? TILE_TORCH_DEBRIS : TILE_DEBRIS));
+ }
}
}
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
@@ -905,7 +930,7 @@ int main(void)
* для неё снят. ПОСЛЕ отрисовки, а не до: слой фона этого же кадра
* взводит бит и обязан быть учтён. Всё, что рисуется ниже
* (mob поверх Кида, борта), взводит его заново само. */
- pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << back));
+ pop_cd_clear(back);
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже