From 4718bff767764aa275cb32b426049321d846d960 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 10 Aug 2026 17:52:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=80=D0=BE=D0=B2=D0=B5=D0=BD=D1=8C=204:?= =?UTF-8?q?=20skip=20=D0=BF=D0=BE=20=D0=BC=D0=B0=D1=81=D0=BA=D0=B5=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20+=20=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82=20loose=5Fland,=20=D0=B2=D0=BE=D1=80=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=200xFF,=20=D0=BF=D0=BB=D0=B0=D0=BC=D1=8F=20=D0=B2=20=D1=84?= =?UTF-8?q?=D0=BE=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ОПТИМИЗАЦИЯ. Метка «фон трогали» была одним union-прямоугольником на страницу, и три факела комнаты склеивались в полосу x 40..280 на всю комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки и каждый кадр платил полным fore-проходом. Теперь это маска тайлов (uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница), проверка — три AND через резидентный pop_cd_hit. Гранулярность тайла — это гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe; подтайловое уточнение там только по высоте, wipe_heights). Замер на (1,7): циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3 растровых кадра. BUG-LOOSE-BUTTON-1. Упавшая плита не нажимала кнопку. Три слоя: порт loose_land не звал trigger_button вовсе; pop_room_col_landing считала площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки, зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего ворота начинали открываться, пока она ещё в воздухе. Нажатие в оригинале ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ» (modifier 0xFF), и кнопка съедается. Посадка в комнате снизу переехала на новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже поля). BUG-GATE-FF-1. 0xFF был перегружен: сторожевое «тайла нет» в gate_modif и живое «открыто навсегда» из trigger_gate. gate_passable заворачивал Кида в воротах, нарисованных открытыми. Мёртвая ветка убрана. BUG-TORCH-CHOMP-1. Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и стирал пламя факела (0,6), которое рисуется в ячейке правого соседа. Фикс — запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале. Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE. Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл tiles_5_stuck в атласе. Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1) -> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host — все 5 наборов. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/perf_backlog.md | 28 ++++++ applications/PoP/roomtest/bug_closed.md | 90 ++++++++++++++++++ applications/PoP/roomtest/bug_list.md | 31 +++++++ applications/PoP/roomtest/pop_cdraw.c | 4 +- applications/PoP/roomtest/pop_cdraw.h | 8 +- applications/PoP/roomtest/pop_level.c | 9 +- applications/PoP/roomtest/pop_map.c | 28 +++++- applications/PoP/roomtest/pop_room.c | 33 ++++++- applications/PoP/roomtest/pop_state.c | 9 ++ applications/PoP/roomtest/pop_state.h | 6 ++ applications/PoP/roomtest/pop_tile.c | 118 ++++++++++++++++++------ applications/PoP/roomtest/pop_trob.c | 10 +- applications/PoP/roomtest/roomtest.c | 37 ++++++-- 13 files changed, 364 insertions(+), 47 deletions(-) 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 уже