From 9ecea17ba6ea9fdaa9687210f3d3ca8a61449e9a Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 11 Aug 2026 23:01:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=80=D0=BE=D0=B2=D0=B5=D0=BD=D1=8C=206:?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=20=D0=BD=D0=B0=20?= =?UTF-8?q?7-=D0=B9,=20=D0=BF=D0=BE=D1=80=D1=82=D0=B0=D0=BB,=20=D0=BC?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=D0=BA=D0=BE=D0=BC=D0=BD=D0=B0=D1=82?= =?UTF-8?q?=D0=BD=D0=BE=D0=B5=20=D0=BF=D0=B0=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BB=D0=B8=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Всё проверено пользователем в MAME. 1. Переход 6 -> 7 падением. leave_room (seg002:0504) для «вниз» на уровне 6 из комнаты 1 даёт особый результат −2, главный цикл (seg000:0893) читает его как ++next_level; вход на 7-м (set_start_pos, seg003:0196) ставит Кида в комнату 17 и сразу переводит экран на комнату под ней. Проверка ОБЯЗАНА идти раньше связи вниз: у комнаты 1 сосед снизу есть (шахта, комната 3). 2. BUG-BALCONY-RIGHT: правая половина портала не рисовалась. id 12 стоял в FORE_ENV_IDS (в tile_table он fore_id ЗЕЛЬЯ), но зелье рисуется из chtab_1, а в chtab_6 под этим номером — правая часть арки балкона 32x62, идущая в backtable. Спрайт уезжал в fore-атлас, env-страница получала дырку (idx 12: w=0 h=0). Найдено трассой POP_TRACE_BT + чтением каталога .atl; дифф комнаты с оригиналом 1151 -> 375. 3. BUG-BELOWROW-WALL: жёлтые треугольники в шахте падения. load_rowbelow (seg008:368) при отсутствующей комнате снизу подставляет tiles_0_empty колонкам 1..9 и tiles_20_wall только левому краю; у нас стеной забивались все 11 позиций. 4. BUG-MOB-MULTIROOM: честный mob_down_a_row (seg007:1387) — кусок переходит в комнату снизу и летит дальше. Плита (1,7) комнаты 6 пролетает шахту комнаты 7 и жмёт кнопку (2,7) комнаты 11, открывая ворота комнаты 18. 5. BUG-MOB-STALE-PAGE: чистка следа куска стояла под гейтом «в отрисованной комнате» — улетевший вниз оставлял себя на одной из страниц навсегда. 6. BUG-CHAR-STALE-PAGE: тень застывала на двух страницах в РАЗНЫХ позах. Прямоугольник и valid пишутся внутри `if (w && h)`, а снимок состояния брался безусловно — при нулевом спрайте слот залипал «тихим». Остаток (почему спрайт нулевой) заведён как SPRITE-ZERO-W0 в BUGS_OPEN. NEXT_SESSION переписан: следующая задача — зацеп за край пола в свободном падении (вход на уровень 7). tests-host 5/5, size-check чист. Co-Authored-By: Claude Opus 5 --- applications/PoP/poc/res/bg/pal_env0.atl | Bin 11292 -> 13280 bytes applications/PoP/poc/res/bg/pal_fore.atl | Bin 13632 -> 11644 bytes applications/PoP/poc/res/bg/pop_env0.atl | Bin 11292 -> 13280 bytes applications/PoP/poc/res/bg/pop_fore.atl | Bin 13683 -> 11695 bytes applications/PoP/roomtest/BUGS_CLOSED.md | 129 ++++++++++++++++++++++ applications/PoP/roomtest/BUGS_OPEN.md | 21 ++++ applications/PoP/roomtest/NEXT_SESSION.md | 36 +++--- applications/PoP/roomtest/TASKS_CLOSED.md | 26 +++++ applications/PoP/roomtest/pop_cdraw.c | 13 ++- applications/PoP/roomtest/pop_guard.h | 11 ++ applications/PoP/roomtest/pop_room.c | 56 +++++++--- applications/PoP/roomtest/roomtest.c | 73 +++++++----- applications/PoP/toolchain/pop_pack_bg.py | 10 +- 13 files changed, 310 insertions(+), 65 deletions(-) diff --git a/applications/PoP/poc/res/bg/pal_env0.atl b/applications/PoP/poc/res/bg/pal_env0.atl index 81f29702bb00085ffdaae7e662ea968f07af6267..70c510c2a7db3089804fea831c681cd3fb5d6193 100644 GIT binary patch delta 1570 zcma)+J5Iwu5Qgm_lsF)oJggug5eo{FNWcZsQWLy3PGUz?R1}=RS3ug9OCT-)9W^K5 z4wN97e;;0r6FX$2m3Q}>`Dga!=i9IQr;qlpe>|~m%X+(f`8@NFj;)E6Q)AOG2(H6` zu^`|>ue)DNV1|haelT`+G(^9}e?}dC;qBUt+oL`odrnnI>qH0!Ax#y@0rc4o5(WVi z348ouG~nl6Q>*Q@6&BIJZX*o&~?Iuy6y9(ln~(8LCEtdI!bRDB7GLj%yNV{W_lI${}7ZV_jh^&Vn>q zAmKS2BGL#?^EA!#xMVZ6Kv@7dv}4jT42S~=T&P3A^$v*mN**N%b^zy4!+{bk5V)kD z2{BcG6cG7K4u;bss46fbTf9Bbi%n#{oXjdk0Lg`85uev1s;rq}=l(1$qbmq=End delta 26 icmaEmJ||+r39cIq3=E8n3=DrXH(t`x+43`mE_IPi$sFfuYQc(5=qAk#U567~~w zWLP4^1#BkfsIbULD{xNC(NS@b;RCDhk%!PXlpwT<8iX#;rXu*Y&5yL%LgPcWZA%Tm&qbja%RaHwEx1s>oSc}yS)eA*W{^0g5T{8(Ggj~S1P zeF7$}eV8QSP5UGa%ls%R^UXem`eGR!9Yr#xK}9$(YgKUtgwR?Ro~R^IDk|BC@R0HW z4%$&^r7%4xfC*SYQXw$T4N?5Hco@gfAWW$wPRU3gpconixT2pbT>uGS{t7|kx&Rm@^Hq?M}vZy*V%Z(nU!Xa*-V_w|0#4bi{>~;wNlEg^>RU@bXY=FfwO1@ zHTWFV;Ha--U*@UVo6IxPjE~2}AQXNXtvl9s=Rv!k1M9aq0dO`19>PP%CHL@qum0}I E6%qi0zyJUM diff --git a/applications/PoP/poc/res/bg/pop_env0.atl b/applications/PoP/poc/res/bg/pop_env0.atl index 81f29702bb00085ffdaae7e662ea968f07af6267..70c510c2a7db3089804fea831c681cd3fb5d6193 100644 GIT binary patch delta 1570 zcma)+J5Iwu5Qgm_lsF)oJggug5eo{FNWcZsQWLy3PGUz?R1}=RS3ug9OCT-)9W^K5 z4wN97e;;0r6FX$2m3Q}>`Dga!=i9IQr;qlpe>|~m%X+(f`8@NFj;)E6Q)AOG2(H6` zu^`|>ue)DNV1|haelT`+G(^9}e?}dC;qBUt+oL`odrnnI>qH0!Ax#y@0rc4o5(WVi z348ouG~nl6Q>*Q@6&BIJZX*o&~?Iuy6y9(ln~(8LCEtdI!bRDB7GLj%yNV{W_lI${}7ZV_jh^&Vn>q zAmKS2BGL#?^EA!#xMVZ6Kv@7dv}4jT42S~=T&P3A^$v*mN**N%b^zy4!+{bk5V)kD z2{BcG6cG7K4u;bss46fbTf9Bbi%n#{oXjdk0Lg`85uev1s;rq}=l(1$qbmq=End delta 26 icmaEmJ||+r39cIq3=E8n3=DrXH(t`x+43`mE_IPi$uFfuYQc(5=qAk!-ZMeQf% z$go@&6SkR{qr&1Zt-v`kM@OYth7YWMzdVHIR))~wY7ly{CLd6}i@|#xCRvbqy1Gn~ zAUau>NeD!D>oReI=!LpWtRNZ$6d2eUCI_;}PtIpK$il&3!?20>BID+he5=#|+$$s# delta 1775 zcmbu9&ubGw6vsEqL|Yq!n50^bHDaJ5Qc`H}66(c+H)-6RWt;5wR0Ik5C(OZrz>uSV zgp^);@#d{p5B?LP_woIh-I>`m74^aJX6EyqH}Abn_VeWDyN_SRpx!$;J@41Vug2PF ze*Rs2Yi@#%x}tq&RI3HSD-i^)pKWzs%x7KkX}dL=&-$YExVJu^J?lU3H%b3thyBG< z_V;($zujws{^j6oKYW1ke(Z-^=y!(UZS=2);X3-0VOU3x)(fr&i$n2n@m_pUz2G3w zuEk>+MTatq<0y)xOL$&z5X38Z{07cPtIrdM<8WSGaRG+-j|7$rNa!*sfD^>3tsuWlrpL#r%&1W*_RASnu0fDE!{NEkbFEX)j)*68 zA8?4BdP70?AORL&0(nKioC~7!m+~abpg`!77o0Am_<%Af5O5{F(k1~4fcIAfhMNRH zP_3X^z~6BN?QmL1@h`2=`HB9C+YWmPDhyWn2|Pi`|JVU~ldB8hH%lB|$dMVhXhI3t zN3P-+-UJyx%E^+Ie;Nw7go_PMN1Nr8t$-6*d}wILQpE~Pzz +## BUG-BELOWROW-WALL. Жёлтые треугольники в шахте падения — ЗАКРЫТ + +**Наблюдение (пользователь, 2026-08-11).** Уровень 6, комната 3 (шахта под +комнатой 1): в нижнем ряду под пустыми тайлами (2,4)-(2,6) торчат жёлтые +треугольники. + +**Корень.** `load_rowbelow` (seg008:368) при ОТСУТСТВУЮЩЕЙ комнате снизу +подставляет РАЗНЫЕ кромки: колонкам 1..9 — `tiles_0_empty`, и только левому +краю (тайл из `room_BL`) — `tiles_20_wall`. У нас `pop_room_load` забивал +стеной все одиннадцать позиций `below_fg`, а стена в роли соседа снизу-слева +даёт `topright` стены — её грань и лезла треугольниками. + +**Фикс.** `below_fg[0..9] = 0`, `below_fg[10] = 20`. + +**Проверено:** комната 3 совпала с картой SDLPoP — 300 пикселей расхождения, +все они силуэт Кида. Опасение «не пропадёт ли законный треугольник в (1,3)» +снято тем же сравнением: грань стены в колонке 3 и скос у её основания на +месте у обоих. Оговорка пользователя: эталон брался из `--screenshot-level`, +живая сверка в SDLPoP — потом. + +--- + + +## BUG-BALCONY-RIGHT. Правая половина портала не рисовалась — ЗАКРЫТ + +**Наблюдение (пользователь, 2026-08-11).** Уровень 6, комната 18: «закрытый +портал показывается некорректно» — у арки балкона нет правой половины, +решётка обрывается, справа чёрный провал. + +**Корень — не в движке, а в УПАКОВЩИКЕ АТЛАСОВ.** `id 12` стоял в +`FORE_ENV_IDS` (`toolchain/pop_pack_bg.py`), потому что в `tile_table` он +числится `fore_id` ЗЕЛЬЯ (0x0A). Но зелье рисуется из `chtab_1` +(`add_foretable(id_chtab_1_...)`; у нас `pop_potion_flask`), а в `chtab_6` +под номером 12 лежит ПРАВАЯ ЧАСТЬ АРКИ БАЛКОНА 32x62, и она идёт в +backtable. Из-за списка спрайт уезжал в `pal_fore.atl`, а `pal_env0.atl` +получал дырку: `idx 12: w=0 h=0`. Движок берёт правую грань соседа из +env-атласа — и не рисовал ничего. + +**Как найдено.** Трасса оригинала `POP_TRACE_BT=1` показала в спорном тайле +`BT room=18 r=1 c=7 ch=6 id=12 w=32 h=62`; дальше — чтение каталога +`pal_env0.atl` напрямую (запись 12 пустая) и `pal_fore.atl` (она там). + +**Фикс.** `id 12` убран из `FORE_ENV_IDS`; в env-атласе он занял своё место. +Дифф комнаты с оригиналом 1151 -> 375 (остаток — Кид и пламя). +**Проверено пользователем:** «портал отрисовался корректно». + +**Урок:** список `FORE_ENV_IDS` собирается по `tile_table.fore_id`, но +`fore_id` НЕ значит «спрайт из chtab_6» — у зелья, меча и пламени он +указывает в `chtab_1`. Прежде чем вносить номер в этот список, проверять, +из какой таблицы спрайт берётся. + +--- + + +## BUG-MOB-MULTIROOM. Плита не пролетала несколько комнат — ЗАКРЫТ + +**Наблюдение (пользователь, 2026-08-11).** Уровень 6: плита (1,7) комнаты 6 +должна пролететь НЕСКОЛЬКО комнат и упасть на кнопку, открывающую решётку. +«Плита просто падает и ничего не открывается.» + +**Корень.** У оригинала кусок живёт в списке mobs и спускается ряд за рядом: +`mob_down_a_row` (seg007:1387) из НИЖНЕГО ряда переводит его в комнату снизу +(`y -= 192`, ряд 0), и так сколько угодно комнат. У нас кусок на границе +комнаты ГАСИЛСЯ, а «переход вниз» подменялся сигналом главному циклу — он +искал посадку в комнате снизу через `pop_room_col_landing`. Для колонки 7 +комнаты 7 (сквозная шахта) посадки нет — кусок исчезал. + +**Фикс.** Честный `mob_down_a_row` в `mob_tick_one` (`pop_room.c`); +приземление в НЕ отрисованной комнате сигналит комнатой и тайлом, а щебень +и кнопку ставит главный цикл (порт `loose_land`, seg007:11E8). + +Путь в этом сценарии: комната 6 (1,7) -> сквозная шахта комнаты 7 -> кнопка +(2,7) комнаты 11, а она по LINKLOC открывает ворота (1,9) комнаты 18 — те +самые у портала. **Проверено пользователем:** ворота открываются. + +--- + + +## BUG-MOB-STALE-PAGE. Улетевшая плита оставалась на одной странице — ЗАКРЫТ + +**Наблюдение (пользователь, 2026-08-11).** Сразу после предыдущего фикса: +«в одном из кадров дабл-буфера осталась падающая плита». + +**Корень.** Чистка прошлого кадра куска стояла под гейтом «кусок в +ОТРИСОВАННОЙ комнате»: `if (here && m->prev_y[pg] != MOB_Y_NONE)`. Как +только кусок уходил вниз через `mob_down_a_row`, гейт закрывался — и след, +оставленный им на этой странице, не стирал уже никто. + +**Фикс.** Гейт убран: `prev_y[pg]` — след ИМЕННО НА ЭТОЙ странице, стирать +его надо независимо от того, где кусок сейчас. Плюс `clean = 2` во всех +ветках гашения, чтобы дочищались обе страницы. **Проверено пользователем.** + +--- + + +## BUG-CHAR-STALE-PAGE. Тень застывала на двух страницах в РАЗНЫХ позах + +**Наблюдение (пользователь, 2026-08-11, редкий — пойман дважды).** Уровень +6, комната с тенью: «Тень застряла в двух разных кадрах в разных позициях». + +**Замер в отладчике (состояние было живым).** `Guard` = кадр 15 (stand), +x = 0x51 — то есть персонаж В ПОКОЕ. А слот отрисовки `pop_cd[OPP]`: + +``` +страница 0: x=25 y=85 w=34 h=38 valid=1 <- ЛЕЖАЩАЯ поза +страница 1: x=41 y=78 w=12 h=41 valid=1 <- стойка +``` + +**Корень.** В `pop_char_draw` (`pop_cdraw.c`) прямоугольник и `valid[dp]` +пишутся ВНУТРИ `if (w && h)`, а снимок состояния для пропуска перерисовки +(`cd_sig_make`) брался БЕЗУСЛОВНО, в самом конце функции. Если спрайт +кадра нулевой (пустая запись атласа), блок рисования пропускался — `x/y/w/h` +и `valid` оставались от ПРОШЛОГО кадра этой страницы, а сигнатура +обновлялась на текущее состояние. Дальше `cd_quiet` видел `valid = 1` и +совпавшую сигнатуру, то есть считал «на странице нарисовано ровно то, что +надо», и слот залипал навсегда: `heal` не звался, старый спрайт оставался. + +**Фикс.** `cd_sig_make` вызывается только когда кадр реально рисовали +(`if (w && h)`). Без снимка слот не будет «тихим», и следующий кадр +начнётся с `heal` — старое сотрётся само. + +**ОСТАЁТСЯ ОТКРЫТЫМ (в BUGS_OPEN):** почему спрайт кадра оказался нулевым. +Фикс убирает залипание, но кадр, для которого атлас отдал 0x0, всё равно +не нарисуется. Подозрение — конкуренция за окно W0 между `atlas_image` и +чтением данных уровня; проверять трассировкой маппинга. diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md index 4166444..6cf1f88 100644 --- a/applications/PoP/roomtest/BUGS_OPEN.md +++ b/applications/PoP/roomtest/BUGS_OPEN.md @@ -14,6 +14,7 @@ | ID | что | тип | статус | |----|-----|-----|--------| +| [SPRITE-ZERO-W0](#sprite-zero-w0) | кадр персонажа изредка отдаёт спрайт 0x0 (залипание уже вылечено) | **редкий** | открыт: нужна трасса маппинга W0 | | [GATE-FORE-KID](#gate-fore-kid) | Кид в проёме ворот виден поверх решётки | окклюзия | открыт: портирован только шовный случай | | [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить | | [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт | @@ -339,6 +340,26 @@ for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row) --- + +## SPRITE-ZERO-W0. Кадр персонажа изредка отдаёт спрайт 0x0 — РЕДКИЙ + +Всплыло при разборе [BUG-CHAR-STALE-PAGE](BUGS_CLOSED.md#bug-char-stale-page) +(тень застыла на двух страницах в разных позах). Залипание слота вылечено — +`cd_sig_make` больше не берёт снимок, если кадр не рисовали, — но САМА +причина нулевого спрайта не найдена: `atlas_image` для валидного кадра +изредка отдаёт запись 0x0. + +Подозрение: конкуренция за окно W0. `pop_char_draw` маппит страницу атласа +(`gfx_w0_map(pages[page].page)`) и держит её до `gfx_w0_unmap()` в конце, а +внутри этого окна успевают отработать `cd_splash` и `pop_sword_draw`; рядом +в кадре W0 маппят чтение данных уровня (`pop_level_tile`, `pop_level_set_tile`) +и mob-тик. Если какой-то путь размапливает окно раньше времени, чтение +заголовка спрайта даст нули. + +Как ловить: watchpoint на порт окна 0 либо счётчик «w==0 при непустом кадре» +в `pop_char_draw` с записью кадра/страницы/idx в отладочную ячейку, дальше +читать её из MAME. Симптом редкий (пользователь поймал дважды). + ## GATE-FORE-KID. Кид, стоящий В ПРОЁМЕ ворот, виден ПОВЕРХ решётки diff --git a/applications/PoP/roomtest/NEXT_SESSION.md b/applications/PoP/roomtest/NEXT_SESSION.md index 46da50b..86b937f 100644 --- a/applications/PoP/roomtest/NEXT_SESSION.md +++ b/applications/PoP/roomtest/NEXT_SESSION.md @@ -1,4 +1,4 @@ -# Точка входа для следующей сессии (обновлено 2026-08-11) +# Точка входа для следующей сессии (обновлено 2026-08-11, вечер) Файл для старта с чистого контекста: где всё стоит, что делать первым, какие грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам: @@ -6,23 +6,22 @@ --- -## 1. СЛЕДУЮЩАЯ ЗАДАЧА: [L5-SHADOW](TASKS_OPEN.md#l5-shadow) — уровень 5 +## 1. СЛЕДУЮЩАЯ ЗАДАЧА: зацеп в ПАДЕНИИ (вход на уровень 7) -Уровни 1-4 приняты **smoke-тестами**; полные обходы всех комнат делаются по -готовности ВСЕХ уровней ([политика приёмок](TASKS_CLOSED.md#pass-policy), -решение пользователя 2026-08-11) — отдельных `L3-PASS`/`L4-PASS` больше нет. +Уровень 6 пройден и принят: спецсобытие с тенью работает, переход на уровень +7 падением работает. Но **вход на 7-й ломается на механике**: Кид влетает в +комнату сверху и, пролетая мимо края пола, ДОЛЖЕН иметь возможность +зацепиться за него руками. Похоже, этого у нас нет вовсе — проверить и +реализовать. -Уровень 5 **не приносит ни одного нового тайла** (инвентарь `res2005.bin` -сверен: всё уже есть с уровней 1-4). Вся работа — одно спецсобытие: тень -появляется в комнате 24, ждёт открытия двери, идёт к зелью, **выпивает его** -и уходит за левый край; боя нет. Писать: `do_auto_moves` + таблица -`shad_drink_move`, `do_init_shad`, ветку `check_shadow` для уровня 5, -`autocontrol_shadow_level5`, плюс ветку ТЕНИ в `check_guard_fallout`. -Всё расписано по SDLPoP прямо в задаче, включая готовые константы. +Куда смотреть: `check_grab` / ветка `action == actions_3_in_midair` в +seg005-seg006 SDLPoP (у нас `POP_ENABLE_JUMP_GRAB` и `check_grab_run_jump` +в `pop_map.c` — это ДРУГОЙ случай, зацеп из прыжка). Начинать, как всегда, +с чтения оригинала, а не с догадок. -Опора: механизм «спецсобытие порождает персонажа в слоте соперника» уже -отработан на скелете (`pop_check_skel`), а сама тень как `charid_1_shadow` -заведена на уровне 4. +Опора: вход падением уже портирован — `FALLING_ENTRY_LEVEL/ROOM` в +`pop_guard.h` + ветка в `pop_start_level` (`roomtest.c`), Кид появляется в +комнате 17 и экран сразу переводится на комнату под ней. ## 2. Состояние репозитория @@ -85,9 +84,10 @@ (seg007:776) не портирован. - **TORCH-ANIM-RIGHT** — под запечённым пламенем могут застыть пики/меч/зелье справа от факела; на уровнях 1-4 такого соседства нет. -- **[GUARD-PHYS](TASKS_OPEN.md#guard-phys)** — остаток: ветка тени в - `check_guard_fallout` (уходит в L5-SHADOW) и живая проверка стража, - нажимающего напольную кнопку. +- **GUARD-PHYS ЗАКРЫТ** 2026-08-11: нажатие напольной кнопки НЕ-Кидом + проверено живьём на уровне 6 (тень встаёт на `closer` и роняет решётку). +- **[GATE-FORE-KID](BUGS_OPEN.md#gate-fore-kid)** — Кид в проёме ворот виден + поверх решётки; портирован только шовный случай. - Долгие: `BUG-SPIKE-1`, `BUG-CHOMP-JUMP-1` (оба низкий приоритет), `L3-COLOR`, `L1-SPEED` (игра на ~39 % быстрее оригинала), `TUNE-1`. diff --git a/applications/PoP/roomtest/TASKS_CLOSED.md b/applications/PoP/roomtest/TASKS_CLOSED.md index ebb3610..cd4cb0d 100644 --- a/applications/PoP/roomtest/TASKS_CLOSED.md +++ b/applications/PoP/roomtest/TASKS_CLOSED.md @@ -1170,3 +1170,29 @@ make-бита для кода `0x6C`. Скратч-байты — хвост `o см. [`../docs/shadow_render.md`](../docs/shadow_render.md)). **Цена:** банк 1 2755 -> 2877 Б, резидент без изменений. + +--- + +### Переход 6 -> 7 падением — СДЕЛАН 2026-08-11 + +Уровень 6 не заканчивается дверью: Кид проваливается вниз из комнаты 1 и +этим попадает на 7-й. Порт двух половин: + +- **выход** — `leave_room` (seg002:0504) для направления «вниз» на уровне 6 + из комнаты 1 возвращает особый результат −2, а главный цикл (seg000:0893) + разбирает его как `Kid.y = -1; ++next_level`. У нас это ветка в обработке + `pop_fell_out` (`roomtest.c`), и она ОБЯЗАНА идти раньше связи вниз: у + комнаты 1 сосед снизу есть (шахта, комната 3), и без спецсобытия Кид + улетал туда, а оттуда — в рестарт уровня; +- **вход** — `set_start_pos` (seg003:0196): на 7-м уровне Кид ставится в + комнату 17, после чего экран сразу переводится на комнату ПОД ней + (`goto_other_room(3)`: y −= 189, ряд пересчитать) — он влетает сверху. + +Константы `FALLING_EXIT_LEVEL/ROOM`, `FALLING_ENTRY_LEVEL/ROOM` — в +`pop_guard.h`. + +**Проверено пользователем в MAME:** «переход на уровень 7 сработал». + +**Остаток на следующую сессию:** влетая в комнату, Кид должен уметь +зацепиться за край пола, мимо которого пролетает — этой механики у нас, +похоже, нет (см. `NEXT_SESSION.md`, п. 1). diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 1f8f5f4..98a2186 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -610,9 +610,16 @@ void pop_char_draw(uint8_t who) __banked } gfx_w0_unmap(); /* Что именно нарисовано на ЭТОЙ странице — снимок для пропуска - * следующих кадров (DRAW-COST). Только на полном пути: ранние выходы - * оставляют valid[dp] снятым, и cd_quiet отсеет слот и без снимка. */ - cd_sig_make(who, &cd_sig[who][dp]); + * следующих кадров (DRAW-COST). Только если кадр РЕАЛЬНО рисовали: + * при нулевом спрайте (`w && h` ложно — пустая запись атласа) блок выше + * не выполняется, и x/y/w/h/valid остаются от ПРОШЛОГО кадра этой + * страницы. Снимок же брался всегда — и слот залипал: cd_quiet видел + * valid = 1 и совпавшую сигнатуру, то есть считал «на странице + * нарисовано ровно то, что нужно», а на ней стоял старый спрайт. Так + * тень оставалась на двух страницах дабл-буфера в РАЗНЫХ позах и мигала + * между ними (BUG-CHAR-STALE-PAGE). Без снимка слот просто не будет + * «тихим», и следующий кадр начнётся с heal — старое сотрётся. */ + if (w && h) cd_sig_make(who, &cd_sig[who][dp]); } void pop_char_fore(uint8_t who) __banked diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 6e9dc40..8bef336 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -103,6 +103,17 @@ #define SHADOW_STEP_LEVEL 6 #define SHADOW_STEP_ROOM 1 +/* Спецсобытие «выход/вход падением» (custom->falling_*, дефолты SDLPoP). + * Уровень 6 не заканчивается дверью: Кид проваливается вниз из комнаты 1 и + * этим переходит на уровень 7, где появляется в комнате 17 — но экран сразу + * переводится на комнату ПОД ней, так что он влетает сверху. Комната 3 + * (шахта под комнатой 1) в переходе не участвует: leave_room перехватывает + * уход раньше, чем сработала бы связь вниз. */ +#define FALLING_EXIT_LEVEL 6 +#define FALLING_EXIT_ROOM 1 +#define FALLING_ENTRY_LEVEL 7 +#define FALLING_ENTRY_ROOM 17 + /* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех * остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см. * pop_check_can_guard_see_kid. */ diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index 1e42cc2..34e7a83 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -879,7 +879,13 @@ static void mob_tick_one(mob_t *m, uint8_t pg) * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; * плита-потолок падает через весь ряд 0, поэтому чистим по фактической * прошлой позиции. */ - if (here && m->prev_y[pg] != MOB_Y_NONE) { + /* Гейта по `here` здесь БЫТЬ НЕ ДОЛЖНО: prev_y[pg] — это след, который + * кусок оставил ИМЕННО НА ЭТОЙ странице, и стереть его надо независимо + * от того, где кусок сейчас. Пока чистка была под `here`, кусок, + * улетевший в комнату снизу (mob_down_a_row), навсегда оставлял себя + * на одной из двух страниц дабл-буфера — «застрявшая плита», видимая + * через кадр (BUG-MOB-STALE-PAGE). */ + if (m->prev_y[pg] != MOB_Y_NONE) { pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); m->prev_y[pg] = MOB_Y_NONE; } @@ -894,14 +900,34 @@ static void mob_tick_one(mob_t *m, uint8_t pg) if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { uint8_t t = mob_tile_at(m); if (t == 0 || t == 11) { - m->row++; /* mob_down_a_row */ + /* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда — + * в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает + * несколько комнат подряд: уровень 6, плита (1,7) комнаты 6 + * проваливается сквозь пустую шахту комнаты 7 и приземляется на + * кнопку (2,7) комнаты 11. Раньше мы кусок на границе комнаты + * ГАСИЛИ и подменяли одним «переходом вниз» из главного цикла — + * дальше первой комнаты он не летел (BUG-MOB-MULTIROOM). */ + m->row++; + if (m->row >= 3) { + m->y = (int16_t)(m->y - 192); + m->row = 0; + m->room = pop_room_link(m->room, 3 /* вниз */); + if (!m->room) { /* улетел из уровня — просто гаснет */ + m->active = 0; m->clean = 2; /* дочистить ОБЕ страницы */ + return; + } + here = (uint8_t)(m->room == pop_t_room); + } } else if (!here) { - /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, сигнальная - * пара pop_loose_landed/pop_debris_at обслуживает только текущую - * комнату — щебень кладём прямо в данные уровня (loose_land пишет - * в curr_room_tiles комнаты curmob.room, а не отрисованной). */ - pop_level_set_tile(m->room, (uint8_t)(m->row * 10 + m->col), 0x0E); - m->active = 0; m->clean = 0; + /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, но щебень и + * КНОПКА под куском обязаны сработать — сигналим главному циклу + * комнатой и тайлом (loose_land, seg007:11E8, работает с + * curmob.room, а не с отрисованной). */ + pop_loose_exit_room = m->room; + pop_loose_exit = (uint8_t)(m->row * 10 + m->col + 1); + /* clean = 2 даже для чужой комнаты: пока кусок летел, он мог + * рисоваться в ЭТОЙ, и след остался на обеих страницах. */ + m->active = 0; m->clean = 2; return; } else if (pop_loose_landed) { /* сигнал прошлого приземления ещё не разобран (один за кадр) — @@ -914,15 +940,11 @@ static void mob_tick_one(mob_t *m, uint8_t pg) return; } } - 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); + /* Страховка: кусок ушёл ниже поля, не поймав границу ряда (в норме этого + * не бывает — mob_down_a_row уводит его в комнату снизу). Просто гаснет, + * как у оригинала кусок в комнате 0 (move_loose: `speed = -2`). */ + if (m->y - 27 >= POP_PLAYFIELD_H) { + m->active = 0; m->clean = 2; return; } if (!here) return; /* чужая комната — считаем, но не рисуем */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index de58c5c..a124d68 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -496,6 +496,19 @@ static void pop_start_level(void) enter_room(start_room); kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir); + /* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м + * уровне Кид ставится в комнату 17, а экран тут же переводится на + * комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) — + * он влетает в неё сверху, продолжая падение с шестого уровня. */ + if (pop_current_level == FALLING_ENTRY_LEVEL && + start_room == FALLING_ENTRY_ROOM) { + uint8_t below = pop_room_link(start_room, LINK_DOWN); + if (below) { + Kid.y = (uint8_t)(Kid.y - 189); + Kid.curr_row = pop_y_to_row((int16_t)Kid.y); + enter_room(below); + } + } Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ /* play_level (seg003:106): have_sword = level >= have_sword_from_level * (=2) — начиная со второго уровня меч у Кида уже есть. */ @@ -759,7 +772,19 @@ int main(void) if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ uint8_t down = pop_room_link(cur_room, LINK_DOWN); pop_fell_out = 0; - if (down) { /* L2: переход в комнату снизу — продолжает падать */ + /* Спецсобытие «выход падением» (leave_room, seg002:0504 — + * направление вниз даёт особый результат −2; разбор в главном + * цикле, seg000:0893). Уровень 6 не заканчивается дверью: Кид + * проваливается вниз из комнаты 1 и этим переходит на 7-й. + * Проверка ОБЯЗАНА идти раньше связи вниз: у комнаты 1 сосед + * снизу есть (шахта, комната 3), и без спецсобытия Кид улетал + * туда, а оттуда — в рестарт уровня. */ + if (pop_current_level == FALLING_EXIT_LEVEL && + cur_room == FALLING_EXIT_ROOM) { + Kid.y = 0xFF; /* seg000:0896 — Kid.y = −1 */ + pop_next_level = (uint8_t)(pop_current_level + 1); + } + else if (down) { /* L2: переход в комнату снизу — продолжает падать */ enter_room(down); /* координаты Kid уже репроецированы pop_map */ gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */ } else { /* выпал из уровня (нет комнаты снизу) — рестарт */ @@ -886,32 +911,28 @@ int main(void) pop_loose_fell = 0; 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); + if (pop_loose_exit) { /* кусок приземлился в НЕ отрисованной комнате */ + /* Комнату и тайл посадки считает сам кусок (pop_room.c): он летит + * через комнаты честным mob_down_a_row, а не «одним переходом + * вниз», как было раньше. Здесь остаётся то, что положено + * loose_land (seg007:11E8): щебень на месте посадки и КНОПКА + * под ним. */ + uint8_t lroom = pop_loose_exit_room; + uint8_t lpos = (uint8_t)(pop_loose_exit - 1); + uint8_t tt = pop_level_tile(lroom, lpos); pop_loose_exit = 0; - if (down) { - int8_t lrow = pop_room_col_landing(down, fcol); - 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)); - } - } + /* На opener подставляется button_type = щебень, то есть «открыть + * НАСОВСЕМ» (modifier 0xFF). Второе место того же порта: своя + * комната — в pop_map.c. */ + if (tt == TILE_OPENER || tt == TILE_CLOSER) + pop_trigger_button(lroom, lpos, + (uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt), + pop_trob_modif(lroom)[lpos]); + /* На факеле остаётся отдельный тайл «факел с щебнем» + * (seg007:1067), а не обычный щебень. */ + pop_level_set_tile(lroom, lpos, + (uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS) + ? TILE_TORCH_DEBRIS : TILE_DEBRIS)); } if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */ uint8_t ccol = (uint8_t)(pop_ceil_fell - 1); diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index 713ce3c..1ae531b 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -44,7 +44,15 @@ OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg") # Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты # столбов, ворот, дебриса. См. tile_table.fore_id + анализ pop_background_strategy. # 119..123 — фронт чомпера и его кровь; 77 — фронт ЗЕРКАЛА (MIRROR_ENV_IDS). -FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100} | {106, 107, 108, 109, 110} \ +# +# id 12 ЗДЕСЬ НЕТ намеренно. В tile_table он стоит как fore_id ЗЕЛЬЯ +# (0x0A), но зелье рисуется из chtab_1 (`add_foretable(id_chtab_1_...)`, +# seg008 draw_tile_fore; у нас — pop_potion_flask), а в chtab_6 под этим +# номером лежит ПРАВАЯ ЧАСТЬ АРКИ БАЛКОНА (32x62), и она идёт в backtable: +# `BT room=18 r=1 c=7 ch=6 id=12` в трассе оригинала. Пока 12 был в этом +# списке, он уезжал в fore-атлас, а env-страница получала дырку — правая +# половина портала не рисовалась вовсе (BUG-BALCONY-RIGHT). +FORE_ENV_IDS = {9, 49, 88, 91, 95, 100} | {106, 107, 108, 109, 110} \ | {119, 120, 121, 122, 123} | {77} # Пол-оверлей подъёма (floor_left_overlay, draw_floor_overlay seg008): рисуется