diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md index 90e3a33..7a798de 100644 --- a/applications/PoP/roomtest/bug_closed.md +++ b/applications/PoP/roomtest/bug_closed.md @@ -398,3 +398,228 @@ MAME по кромкам. Следствие: в 13/18/24 возможен «мусор в шве» — наш рендер кромки читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не считает. Приоритет багов в этих трёх комнатах — низкий (в игре не видно). + +--- + +## Проверено в MAME 2026-08-03 (прогон уровня 1: 11 наблюдений → 6 корней) + +Сырой список наблюдений с обхода всех комнат разобран по корням. +Проверка — MAME + мост `mame-z80`: чтение `_Kid` по адресу из +`.sprinter-cc-roomtest/roomtest.noi`, `ROOMNAV` для навигации, потиковые +трассы, скриншоты. + +**Оговорка о полноте проверки.** Каждый корень закрыт тем, что его СОБСТВЕННЫЙ +сценарий больше не воспроизводится; сквозного прохождения уровня и поиска +регрессий в соседней механике автоматика не делала. Чек-лист для ручной +перепроверки — в [`bug_list.md`](bug_list.md), раздел «Ручная перепроверка +фиксов». Особое внимание — порту `check_collisions`: он переписал ВСЮ +горизонтальную коллизию. + +### BUG-LVLSTATE-1. Уровень был немутабельным — **ЗАКРЫТ** + +**Симптомы:** выпитое зелье / поднятый меч / разбитая плита возвращались при +возврате в комнату; иногда кувшина нет, а пузырёк над ним крутится; в +комнате 15 раз в 1–2 с мигал контур кладки. «Первое остаётся, остальное +возвращается». + +**Корень.** Уровень лежал в EMM-странице только на чтение, `enter_room` +перезаливал `room_fg[30]` из неё при каждом входе, а персистентность держала +таблица переопределений на **8 записей** (`OVR_MAX` в `roomtest.c`) — девятая +и дальше молча терялись. Второй хвост: `pop_trob.c` брал тип тайла из +СТРАНИЦЫ (`pop_level_tile_raw`), поэтому заводил trob зелья/меча там, где +предмет уже поднят — отсюда пузырёк без кувшина и мигание от `animate_sword`. + +**Фикс.** `pop_level_set_tile()` пишет тайл ПРЯМО в страницу уровня (порт +`curr_room_tiles[…] = …`: `do_pickup` seg006:1671, `remove_loose` seg007:0EB8, +`loose_land` seg007:11E8). Таблица `ovr_*` удалена. Эталонная копия +foretable — в той же странице по смещению `0x1000` (страница 16 КБ, данных +2.3 КБ). + +**Проверено:** комната 22, зелье (0,6) выпито → выход в 23 → возврат: кувшина +нет, пузырька нет; ROOMNAV ставит Кида уже на (0,6), потому что тайл стал +полом. + +### BUG-RESPAWN-1. Респавн не перезагружал уровень — **ЗАКРЫТ** + +**Как в оригинале:** цикл `play_level` (seg003:57) на КАЖДОЙ итерации, в том +числе после смерти, зовёт `load_level()` — уровень читается заново. + +**Фикс.** `pop_level_reset_tiles()` (восстановление foretable из эталонной +копии) в `pop_start_level` рядом с `pop_trob_reset()`. + +**Проверено:** после смерти от стража зелье в комнате 22 снова на месте. + +### BUG-DEATH-1. Смерть от меча не доводилась до конца — **ЗАКРЫТ** + +**Симптом:** страж убивает Кида, тот «воскресает» на месте и его убивают +снова, по кругу. + +**Корень.** `hurt_by_sword` ставил seq_85 и обнулял HP, но `Kid.alive` +оставался −1, а `pop_kid_dead` (по нему главный цикл делает респавн) взводил +только путь пик/падения. В оригинале это первая строка `control_kid` +(seg006:0CD1): `if (Char.alive < 0 && hitp_curr == 0) Char.alive = 0;`. + +**Фикс.** Порт этой ветки в начало `pop_ctrl_tick` + `pop_kid_dead = 1`. + +**Проверено:** страж в комнате 21 убивает Кида → респавн в стартовой позиции +уровня (комната 1), цикла нет. + +### BUG-GATE-ANIM-1. Ворота в отрисованной комнате не перерисовывались — **ЗАКРЫТ** + +**Корень.** `pop_process_trobs` продвигал модификатор ворот, но пометки +перерисовки не ставил; ворота рисовались только при полной отрисовке комнаты +и в `pop_room_redraw_seam_left`. В оригинале `animate_door` (seg007:0522) +заканчивается `draw_trob()` (seg007:01E6). + +**Фикс.** Вид перерисовки `POP_RD_GATE` + `pop_gate_redraw(row,col)` в +`pop_bg.c` (wipe зоны решётки в ячейке ПРАВОГО соседа + `draw_tile`), пометка +из `pop_process_trobs` (текущая страница каждый кадр, обе — на последнем). + +**На уровне 1 это ровно ОДНА решётка:** room5 (0,5). Все остальные стоят в +колонке 9, их бары рисуются уже в соседней комнате — там работает +`pop_room_redraw_seam_left`. + +**Проверено:** кнопка room5 (0,4) — решётка (0,5) поднимается на экране. + +### BUG-COLL-1. Bump искал стену только в колонке переднего края — **ЗАКРЫТ** + +**Симптомы:** пробегание сквозь закрытую решётку (комната 12, room5 (0,9)); +влёт внутрь стены на длинном прыжке (комната 6). + +**Два корня.** +1. `check_bumped` брал ОДНУ колонку — ту, в которой оказался передний край. + Для решётки в col9 окно этой колонки всего 4 px (201..204), беговой кадр + его перескакивает — бампа нет, а `check_leave` тут же уводит в соседнюю + комнату. Оригинал (`check_collisions`, seg004:0004) считает флаги + перекрытия для ВСЕХ колонок ряда и берёт колонку с переходом флага 0→1. +2. Наш guard `action == FREEFALL || MIDAIR → return` в `check_bumped`. В + оригинале таких гардов НЕТ: `bumped_fall` (seg004:04E4) специально + разбирает `actions_4_in_freefall`. Отсюда влёт в стену в прыжке. + +**Фикс.** Полный порт `check_collisions` + `get_row_collision_data` + +`is_obstacle` + `bumped(delta, push_dir)`; колонки считаются от −2 до 11, +чтобы решётки СОСЕДНЕЙ комнаты (швы) бампили как свои; гарды в `check_bumped` +приведены к оригинальным (только вис и подтягивание). + +**Проверено:** комната 5, бег вправо в закрытую решётку (0,9) — Кид упирается +(x встаёт на 60 в системе комнаты 1 и дальше не растёт). + +**Остаток:** экран при этом перелистывается на соседнюю комнату, и Кид в шве +не рисуется — отдельный баг BUG-SEAM-DRAW-1 (модель straddle S3), см. +`bug_list.md`. + +### BUG-STANDUP-1. Вставание из приседа у стены роняло сквозь пол — **ЗАКРЫТ** + +**Симптом:** комната 5, падение с кнопки (0,6) на щебень (2,7) с уроном, +присед — и при вставании провал в комнату 6. + +**Корень — лишний guard `if (Kid.action == ACT_BUMPED) return;` в +`bumped_floor`** (в оригинале, seg004:0520, там `if (Char.alive)`). Цепочка: +вставание двигает Кида на 1 px в стену → бамп → `bumped_floor` прижимает `y` +к полу, отменяя `dy(−2)` из начала `medland` → наш guard возвращает +управление вместо `seq_47`, `medland` доигрывает свои `dy(+1)`,`dy(+1)` уже +ОТ пола → Кид НИЖЕ пола → следующий бамп читает беззнаковую разность как +«высоко над полом» → `bumped_fall` → выпадение вниз. У оригинала `seq_47` +обрывает `medland`, лишних `dy` нет. + +Найдено потиковой трассой + временной диагностикой (колонка бампа = 8, тайл +после разрешения = 14 щебень, ветка = `bumped_floor`). + +**Заодно** убран полу-порт опционального `FIX_STAND_ON_THIN_AIR`: у нас была +взята только его первая часть (кадры вставания 110..119 требуют пол), без +парной правки seqtbl (`dx(1)→dx(0)`, `dx(−4)→dx(−3)`), которую применить +нельзя — seqtbl извлечён из данных оригинала. Вернулись к ванильному +`seg006:909` (только кадр 109). + +--- + +# Вторая волна прогона 2026-08-03 (вечер) + + +## BUG-KBD-4 (+ BUG-KBD-3). Shift: сначала залипал, потом стал отваливаться — **ЗАКРЫТ** + +**Симптом (вторая редакция).** Залипание ушло, но появилось обратное: при +зажатом Shift второй-третий-четвёртый тап стрелки отрабатывал уже не +осторожным шагом, а бегом. Для PoP это ХУЖЕ залипания: игрок рассчитывает +на короткий шаг, а Кид убегает в яму или на пики. + +**Почему обе прежние редакции были неправильны.** Это был размен между +двумя способами «починить» потерю байта при Rx-overrun SIO (FIFO 3 байта): + +1. исключать модификаторы из сброса — потерянный break Shift снять нечем, + Shift залипает навсегда (BUG-KBD-3); +2. сбрасывать всю карту, как DSS (`KBD_Receiver_Overrun` в `KEYINTER.ASM` + чистит и `KEYCTRL`, и `KEY_FLG`) — залипания нет, но Shift сносится + каждым overrun'ом (BUG-KBD-4). + +Причина, по которой вариант 2 бил так часто, замерена: при зажатом Shift +клавиатура обёртывает КАЖДЫЙ расширенный код служебной парой, поэтому тап +стрелки — это не 5 байт, а 10, и overrun почти гарантирован. + +**Что нашлось (замер в MAME, watchpoint на порт данных SIO + чтение карты +`_kbdraw_down`).** Эта самая обёртка — «fake shift» — и есть решение, а не +помеха. Клавиатура шлёт `E0 F0 12` перед расширенным make и `E0 12` после +его break, и шлёт ТОЛЬКО пока Shift реально зажат. Проверено обоими +шифтами: правый обёртывается своим кодом `0x59` (bit 1 байта 11 карты), левый +— `0x12` (bit 2 байта 2). Это непрерывное и прямое свидетельство реального +состояния Shift — единственное доступное, потому что опросить состояние у +PS/2 нельзя, а typematic повторяет только ПОСЛЕДНЮЮ нажатую клавишу, то есть +стрелку, а не Shift. + +**Фикс.** Оба декодера (`libc/irq/_irq_tramp.c`, `libc/kbd/kbd_raw_poll.c`) +читают обёртку в обе стороны: + +- увидели fake shift → реальный Shift ЗАЖАТ → взвести plain-бит (бит кладёт + общий писатель: достаточно обнулить префиксы, и он попадёт в plain-половину + карты как make; в расширенную половину не пишем — это не клавиша); +- расширенный make БЕЗ предшествующей обёртки → Shift ОТПУЩЕН → снять + plain-биты обоих шифтов. + +`kbd_raw_sync` вернулся к исключению модификаторов — теперь это безопасно: +состояние Shift подтверждается независимо от того, что съел overrun, а +залипание снимается первым же нажатием стрелки. + +**Побочно найдена и исправлена своя ошибка** в новом коде `kbd_raw_poll`: +после `bit 1, a` в `A` лежал `pending`, а не скан-код, — обычные +(нерасширенные) клавиши декодировались бы из мусора. Оба декодера приведены +к одной логике. + +**Раскладка трамплина.** Клавиатурный блок перевалил за 127 байт, а `jp` +внутри трамплина запрещён (код копируется в W2 побайтно, абсолютные +само-ссылки сломают копию). Поэтому префиксные обработчики переехали вплотную +к своим `cp`, а посередине тела стоят три ретранслятора (`tr_kbd_hub`, +`tr_hub_notkbd`, `tr_hub_dss`) — до них дотягиваются `jr` и сверху, и снизу. +В `kbd_raw_poll` такого ограничения нет, там три перехода стали `jp`. + +**Проверено в MAME:** + +- Shift зажат, пять тапов стрелки подряд → `LSh` остаётся `04` во всех пяти, + `ovr = 0`, расширенная половина карты чистая; +- штатное отпускание Shift → `LSh = 00`; +- искусственно залипший Shift (бит записан в карту отладчиком) → снимается + ПЕРВЫМ же тапом стрелки; +- `make size-check`: 70 программ, роста нет. + + +## BUG-RESPAWN-2. После respawn стражи остаются мёртвыми — **ЗАКРЫТ** + +**Вопрос из отчёта:** «после respawn — должны ли оживать стражники?» +**Ответ по SDLPoP: да.** Цикл `play_level` (seg003:57) на КАЖДОЙ итерации — +в том числе после смерти Кида — делает `load_level()`, следом `pos_guards()` +(seg003:83), а затем `Guard.charid = charid_2_guard; Guard.direction = +dir_56_none`. То есть стражи — такая же часть данных уровня, как тайлы, и +перезагрузка файла возвращает их всех. + +**Корень у нас.** `gstate_init()` (живая копия таблицы стражей: тайл, +направление, мастерство, поза трупа) вызывался ТОЛЬКО при загрузке уровня. +Рестарт возвращал тайлы (`pop_level_reset_tiles`), но не стражей: в `gstate` +оставался сохранённый `leave_guard`'ом `seq_hi != 0`, и `pop_guard_enter` +поднимал стража трупом с `guardhp_curr = 0`. + +**Фикс.** `pop_level_reset_guards()` (тот же `gstate_init`) + `pop_guard_reset()` +в `pop_start_level`, рядом с `pop_level_reset_tiles()`. Туда же уехал +`pop_loose_mob_reset()` — настоящий `mobs_count = 0` из `start_level`. + +**Проверено в MAME:** комната 21, страж жив (`alive = -1`, hp 3) → убит читом +`K` (`alive = 0`, hp 0) → Кид убит стражем → респавн → возврат в комнату 21: +страж снова `alive = -1`, hp 3. diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index d3708f0..f4aaa1b 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -1,30 +1,169 @@ # roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации -Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней — -odd-pixel арифметика `char_x`, подстановка тайла нажатой кнопки, баг -кодогенератора SDCC) переехало в [`bug_closed.md`](bug_closed.md): прежде чем -заводить новый баг, грепни там по симптому. +Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней) +переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг, +грепни там по симптому. Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта: механику сверять с `../SDLPoP/src/` ДО кодинга. -Ревизия списка: **2026-08-01** (задача L1-TRIAGE). Закрыты и перенесены в -архив семь записей: BUG-1, BUG-2, BUG-3 (стояли как Critical с 2026-07-21, -прогнаны в MAME) и вся косметика окклюзии BUG-CEIL-1/2/3, BUG-OCCL-1 (была -починена кодом ещё в июле, список просто отстал — чем именно закрыта каждая, -расписано в [`bug_closed.md`](bug_closed.md)). +Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1). Одиннадцать +наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены +в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень +стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень), +BUG-DEATH-1 (смерть от меча), BUG-GATE-ANIM-1 (анимация ворот), BUG-COLL-1 +(полный порт `check_collisions`), BUG-STANDUP-1 (лишний guard в +`bumped_floor`). Клавиатурный BUG-KBD-3 доведён до конца во второй волне +(см. ниже); из первой волны открытым остался косметический BUG-SEAM-DRAW-1. -**Открытых багов нет.** Осталось только незакрытое по оптимизации и -незаконченная ревизия комнат: +Ревизия **2026-08-03, вечер** (вторая волна с прогона): четыре наблюдения → +BUG-KBD-4 (фантомный отжим Shift) и BUG-RESPAWN-2 (стражи не оживают) — +**закрыты и проверены в MAME**; BUG-DRAWORDER-1 (Кид поверх тела стража) и +BUG-LOOSE-2 (осколки только от одной из двух плит) — **исправлены по SDLPoP, +но их сценарии автоматикой не воспроизведены** (см. таблицу ниже). -| ID | что | тип | -|----|-----|-----| -| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | -| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | -| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | +| ID | что | тип | статус | +|----|-----|-----|--------| +| [BUG-DRAWORDER-1](#bug-draworder-1) | Кид рисуется поверх тела стража (в оригинале — за телом) | Minor | фикс есть, **сценарий не проверен** | +| [BUG-LOOSE-2](#bug-loose-2) | осколки только от одной из двух падающих плит (комната 12) | Minor | фикс есть, **гонка не воспроизведена** | +| [BUG-SEAM-DRAW-1](#bug-seam-draw-1) | у решётки на шве экран переключается, а Кид в шве НЕ рисуется | Minor | открыт | +| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт | +| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт | +| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт | --- + +# Ручная перепроверка фиксов (2026-08-03) + +Шесть корней закрыты автоматической проверкой в MAME (мост `mame-z80`: +чтение `_Kid`, потиковые трассы, скриншоты) — этого достаточно, чтобы +показать, что конкретный сценарий больше не воспроизводится, но НЕ +достаточно, чтобы поймать регрессии в соседней механике. Ниже — сценарии +ровно в тех формулировках, в которых баги были заведены. + +Собрано и лежит в образе (`make` + `make hdd` уже прогнаны; libc пересобран, +`make size-check` без роста). + +| # | что проверить | ожидаемо | +|---|---------------|----------| +| 1 | комната 22: выпить зелье (0,6), выйти в 16/23 и вернуться | кувшина нет, пузырька над пустым местом нет | +| 2 | то же для комнат 14 (0,5) и 17 (2,3) | так же | +| 3 | комната 15: подобрать меч (2,2), выйти и вернуться | меча нет; кладка на дальней стене НЕ мигает | +| 4 | комната 12: разбить плиты (0,1)/(0,2) и потолок в 16, выйти-вернуться | остаются разбитыми, проём не закрывается | +| 5 | комната 17 из 23: разбить (1,5)/(1,6), выпить зелье (2,3), вернуться | всё остаётся | +| 6 | **после смерти** зайти в те же комнаты | ВСЁ восстановлено (в оригинале смерть = `load_level`) | +| 7 | комната 12: разбег в закрытую решётку (0,9) с полушага | не проходит насквозь; экран может перелистнуться — это [BUG-SEAM-DRAW-1](#bug-seam-draw-1) | +| 8 | комната 6: бег справа налево от (0,9), длинный прыжок (0,6)→(0,7) | не влетает внутрь стены | +| 9 | комната 5: с кнопки (0,6) падение на (2,7), присед, вставание | остаётся в комнате 5 (проверено трассой: `fr=111 x=177` → `seq_47` → `fr=15 x=173`) | +| 10 | комната 5: нажать кнопку (0,4) | поднимаются ОБЕ решётки — (0,5) видно на экране, (0,9) проверять из комнаты 1 | +| 11 | страж (комнаты 3, 21) убивает Кида | смерть доигрывается, респавн в стартовой позиции уровня; цикла «убил-воскрес» нет | +| 12 | клавиатура: долгая игра с Shift+стрелка | ↑ и Shift не залипают; осторожный шаг не превращается в бег (доработано во второй волне — п.17) | + +## Вторая волна (2026-08-03, вечер) + +| # | что проверить | ожидаемо | +|---|---------------|----------| +| 13 | **[BUG-DRAWORDER-1]** убить стража там, где через тело можно пройти С ЛЕВОЙ стороны (не у самой кромки), и провести Кида сквозь тело | Кид проходит ЗА телом, когда его тайл в обходе РАНЬШЕ (левее по колонке или ниже по ряду), и поверх — когда позже | +| 14 | **[BUG-LOOSE-2]** комната 12: разбежаться по плитам (0,1)/(0,2) и уйти в 15 ДО того, как долетит вторая; вернуться | осколки на ОБОИХ — (1,1) и (1,2) | +| 15 | то же в комнате 7 (плиты 0,5/0,6 → 2,5/2,6) | не сломалось: по-прежнему двое осколков | +| 16 | плита, улетевшая из комнаты вниз, при возврате | лишнего щебня не появилось | +| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки | +| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные | + +Отдельно стоит посмотреть **на регрессии от порта `check_collisions`** — он +трогает всю горизонтальную коллизию: бамп в стену на бегу и в прыжке, +осторожный шаг у стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме +решётки, переходы через швы (старый BUG-SEAM-PINGPONG). + +--- + +# Открытые баги уровня 1 + + +## BUG-DRAWORDER-1. Кид рисуется поверх тела стража + +**Симптом.** В оригинале Кид проходит ЗА телом убитого стража; у нас — перед +ним. + +**Корень.** Мы рисовали безусловно `pop_guard_draw()` → `kid_draw()`, то есть +Кид всегда сверху. В оригинале «кто поверх» никем не назначается: и Кид, и +страж кладутся в midtable при обработке СВОЕГО тайла +(`set_objtile_at_char`, seg006:13F3), а тайлы обходятся строго — +`redraw_needed_tiles` (seg008:1B06) идёт рядами 2,1,0 и внутри ряда +колонками 0..9. Нарисованный позже оказывается сверху. Если тайлы совпали, +решает `sort_curr_objs` (seg008:203C): у обычных объектов (не тень, не плита) +ниже по `obj_y` — значит позже. + +**Фикс** (`roomtest.c`): `guard_over_kid()` считает ключ обхода +`(2 - row) * 10 + col` для обоих персонажей (с поправкой +`set_objtile_at_char` на лазание/полёт/вис — колонка левее), сравнивает, при +равенстве сравнивает `obj_y`, и порядок вызова `pop_guard_draw`/`kid_draw` +выбирается по результату. + +**Известное упрощение.** Ветку `actions_1_run_jump` (там оригинал берёт +нижний ряд и левую колонку ГАБАРИТА, а не `curr_row`/`curr_col`) не +воспроизводим: она требует полного футпринта, а влияет только когда +прыгающий Кид делит тайл со стражем — там порядок всё равно разводит `obj_y`. + +**Не проверено.** Сценарий не воспроизведён: в комнате 21 страж падает у +самой левой кромки (col 0), слева от тела встать нельзя, а «Кид поверх» при +Кид.col=1 > страж.col=0 — это как раз ПРАВИЛЬНЫЙ порядок и бага не +показывает. Нужен ручной прогон, п.13 таблицы выше. + + +## BUG-LOOSE-2. Осколки только от одной из двух падающих плит + +**Симптом.** Комната 12, соседние падающие плиты (0,1) и (0,2): после +пробежки осколки появляются только на (1,2), на (1,1) — нет. В комнате 7 +(плиты 0,5/0,6 → 2,5/2,6) обе дают осколки. + +**Корень** (гипотеза автора отчёта подтвердилась чтением кода). Наш +падающий кусок был привязан к ТЕКУЩЕЙ комнате: `pop_loose_reset()` на смене +комнаты звал `pop_loose_mob_reset()` и гасил всё, что ещё летит, а тайл под +куском читался из карты отрисованной комнаты. Кид убегает в комнату 15 +раньше, чем долетает плита (0,1) — кусок уничтожается, `loose_land` не +случается, щебень не кладётся. В комнате 7 обе плиты успевают сесть до +ухода, поэтому там всё выглядело правильно. + +В оригинале `mobs[]` чистится ТОЛЬКО в `start_level` (seg003:88), `do_mobs` +(seg007:1063) прокручивает все куски независимо от `drawn_room`, `move_loose` +работает по `curmob.room`, а `redraw_at_cur_mob` (seg007:132C) сверяется с +`drawn_room` лишь для ОТРИСОВКИ. + +**Фикс.** У куска появилось поле `room` (`curmob.room`); на смене комнаты +зовётся `pop_loose_mob_room_changed()` — сбрасывает только heal-историю; +тайл читается через `mob_tile_at()` (своя комната — `tile_code`, чужая — +`pop_level_tile`); приземление в покинутой комнате пишет щебень прямо в +данные уровня; отрисовка и `check_loose_fall_on_kid` (там оригинал начинает с +`Char.room == curmob.room`) — только для своей комнаты. Настоящий +`mobs_count = 0` переехал в `pop_start_level`. + +**Не проверено.** Гонка «уйти из комнаты раньше, чем долетит плита» +автоматикой не воспроизведена. Ручной прогон — пп. 14–16 таблицы выше. + + +## BUG-SEAM-DRAW-1. Кид, упершийся в решётку на шве, не рисуется + +**Симптом.** Комната 5: Кид добегает до закрытой решётки (0,9), решётка его +ДЕРЖИТ (проверено: x встаёт на 60 и дальше не растёт), но экран +переключается на комнату 1, и в ней Кид не виден — он стоит на `x = 57..60`, +левее кромки комнаты (col 0 начинается с 58). + +**Разбор.** Переключение экрана само по себе штатное: `leave_room` +(seg002:423) уводит вправо при `char_x_right >= 201`, а левая грань решётки +как раз 201 — то есть в оригинале экран тоже перелистнётся, если персонаж +встанет ровно на грань. Не штатно то, что персонажа в шве **не рисуют**: +`update_kid_render_dx` даёт смещение только когда `kid_room != cur_room`, а +`enter_room` сразу выравнивает `kid_room = cur_room`. Это остаток +непортированной модели straddle (S3 в `pop_kid_plan`). + +**Что делать.** Довести S3: держать `Kid.room` отдельно от `drawn_room` и +рисовать персонажа со смещением ±140, как `xpos_in_drawn_room` (seg004:0405). + + + + # Оптимизация отрисовки (записано 2026-07-29) @@ -139,3 +278,26 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ - **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и `S`. Все они со временем съедутся в общий блок читов, разрешаемый в настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`). + + +--- + + +## Сырые наблюдения (прогон 2026-08-03) → корень + +Формулировки с прогона, как записаны. Закрытые корни — в +[`bug_closed.md`](bug_closed.md). + +| # | наблюдение (кратко) | корень | статус | +|---|---------------------|--------|--------| +| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт | +| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт | +| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт | +| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт | +| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт | +| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт | +| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) | +| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) | +| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт | +| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт | +| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт | diff --git a/applications/PoP/roomtest/bugs_level1.md b/applications/PoP/roomtest/bugs_level1.md new file mode 100644 index 0000000..2a39226 --- /dev/null +++ b/applications/PoP/roomtest/bugs_level1.md @@ -0,0 +1,80 @@ +Активные баги на уровне 1 - + +- После того как кувшин со здоровьем выпит и Кид попадает в комнату во второй раз + то иногда кувшин мы не показываем (это правильно) но показываем анимацию пузырьков над + кувшином (чего показывать больше не должны - нет кувшина, нет пузырьков), + а в комнате 14 пузырек вообще восстанавливается полностью после того как Кид + выйдет из комнаты и вернется в нее обратно (есть ощущение что даже в одной комнате + поведение пузырька - будет ли он восстановлен или нет зависит от чего-то непонятного. + (есть еще ощущение что первый выпитый кувшин не восстанавливается а остальные + восстанавливаются) +- Меч - один раз подобранный он больше не должен появляться в своей (15-ой) комнате + иногда он появляется снова, иногда он больше не появляется но на дальней стене начинают + моргать контуры кирпичной кладки (странно что она не впечатана в heal уровень потому + что она должна быть отрисована всегда). причем мерцает по странному - не как double-buff + а как буд-то вместо искры на мече - изредка на несколько (один логический?) кадров. +- комната 12 - опять проблема с телепортацией через решетку - решетка в 0,9 - Кид стоит + за шаг (пол-шага) от решетки - бежит и пробегает через закрытую решетку. +- после respawn - остаются разбитыми плиты что были разбиты, выпитыми пузырьки и прочее - + это надо сравнить с оригиналом (SDLPoP) - но главное другое - если до respawn какая-то + плита не была разбита то теперь если ее разбить, выйти из комнаты и вернуться - то + плита восстановится (еще интереснее вариант когда плита-потолок в комнате 12 разбивается, + Кид через проем поднимается в верхнюю комнату и поднявшись плита в новой комнате опять + закрывает проем. +- проблема с плитой из комнаты 12 в верхнюю и пузырек в комнате 22 - они возникают даже + без respawn - когда Кид заходит в комнату 22 из комнаты 16, выпивает пузырек, выходит опять + в комнату 16 и опять возвращается в комнату 22 - пузырек возникает снова. Когда Кид разбивает + плиту из комнаты 12 в комнату 16 то после подъема в комнату 16 плита опять возникает. + аналогично комната 17 - если войти в нее из комнаты 23, разбить плиты 1,5 и 1,6, опуститься на + 2,3 и выпить пузырек. вернуться в комнату 23 и опять в 17 - то и плиты вернутся и пузырек. + но могут плита и пузырек не вернуться - такое ощущение что если это первая разбитая плита + (Кида мы перенесли в комнату 12 через cheats) - то разбитая плита остается разбитой. +- комната 6 - Кид бежит по уровню ноль справа налево (начало бега 0,9), делает длинный + прыжок между 0,6 и 0,7 - он пролетает во внутрь стены и респавнится - скриншоты в bugscreens +- иногда залипают все-таки клавиши (пока замечено на кнопке вверх - отлипает только после + следующего нажатия). надо проверить - есть ли возможность не только отслеживать события + нажатие/отжатие но и текущий статус нажатых кнопок. Может вообще отказаться от прямой работы + с клавиатурой а работать с нею через BIOS ? +- иногда таки залипает и shift =(((( +- комната 5 - кнопка на 0,4 не открывает решетку/ворота на 0,5 - это двойная кнопка - она + должна сразу открывать ворота на 0,5 и на 0,9 (возможно у нас она работает только на 0,9) + возможно что логически решетка на 0,5 открывается но анимации ее открытия нету =( +- комната 5 - Кид падает с кнопки 0,6 на 2,7 - получает урон, присяд в 2,8 - когда встает + он проваливается еще ниже в комнату 6 на между 2,7-2,8. +- если стражник убивает Кида то Кид респавнится на месте смерти и стражник тут же его убивает + еще раз и так по циклу + +-------- + +- проверить как отрисовывается Кид вместе с мертвым стражником - в оригинале стражник поверх + Кида (Кид проходит за телом стража), у нас Кид поверх (перед) стражем; +- сейчас у нас шифт не залипает но теперь он теряет статус нажатости - то есть при нажатом шифте + второе-треть-четвертое нажатие стрелки отрабатывает не как осторожный шаг а переводит Кида на бег + по идее с новым алгоритмом должно было бы корректно отрабатывать нажатие шифта ? теперь же залипаний + нету но есть фантомный отжим ? такое поведение работы с клавиатурой хуже чем залипание шифта (потому + что игрок надеется что шифт нажат и будет короткий шаг а Кид начинает бежать и может провалиться в + ловушку/яму или набежать на пики и погибнуть). то есть надо как-то более точно проверять статус нажатости + шифта. + то есть - когда нажат шифт и нажимаются стрелки то вариант редких пропусков стрелок предпочтительнее чем + вариант когда мы перестаем понимать что шифт нажат. +- после respawn - должны ли оживать стражники ? сейчас они остаются мертвые. +- небольшой баг отображения (такой баг у нас уже был и ты его фиксил) - комната 12 - две падающие плиты + рядом (0,1 и 0,2) - после падения осколки только от одной на 1,2 а от 0,1 на 1,1 осколков нету. + при том что в комнате 7 две плиты 0,5 и 0,6 падая на 2,5 и 2,6 дают двое осколков. + возможная причина - когда Кид пробегает в комнате 12 по падающим плитам то он убегает сразу в комнату 15 + и плита 0,2 успевает упасть и поменять статус пола 1,2 на пол с осколками а плита 0,1 еще остается в падении + когда Кид уже уходит в комнату 15 и потому статус пола на 1,1 на 'пол с осколками' измениться не успевает + +-------- + +Подумай вот над чем - + интересует возможность покрытия уже написаного кода автотестами (тесткейсами). + автотесты внутри MAME я думаю будут достаточно затруднительными, но поскольку это у нас plain-C + то можно наверное делать автотесты и на host OS. + возможно это нам поможет не создавать ситуации когда нам каждый раз приходится фиксить баги которые + мы уже фиксили (по крайней мере постараемся избегать регресса при реализации новых фич) + я понимаю что это скорее всего будет достаточно сложно совместить с тем что финальный продукт идет + для Sprinter. но подумать над этим надо. + +Пока ничего не предпринимай, только подумай что и как можно сделать. + diff --git a/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.16.50.png b/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.16.50.png new file mode 100644 index 0000000..192bd88 Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.16.50.png differ diff --git a/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.17.01.png b/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.17.01.png new file mode 100644 index 0000000..9f51e75 Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.17.01.png differ diff --git a/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.17.10.png b/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.17.10.png new file mode 100644 index 0000000..ed2914c Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/Снимок экрана — 2026-08-02 в 00.17.10.png differ diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index e313d2a..0a65782 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -922,6 +922,35 @@ void pop_leveldoor_redraw(int row, int col) __banked gfx_set_bank(GFX_BANK_NORMAL); } +/* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob, + * seg007:01E6 — им заканчивается animate_door). (row,col) — сами ворота; + * решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому + * wipe и redraw идут по ней. Логика ровно как у левого шва + * (pop_room_redraw_seam_left), только колонка произвольная. + * + * Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при + * ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары. Полоса — ровно + * зона решётки: от gate_top_y (dby−62 = 63*row+3) до низа (63*row+64); + * выше начинается ряд −1, ниже — стена ряда ниже. + * + * Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их + * не видно — оригинал такой trob тоже пропускает + * (get_trob_right_pos_in_drawn_room → 30). Для взгляда из комнаты справа + * работает pop_room_redraw_seam_left. */ +void pop_gate_redraw(int row, int col) __banked +{ + int x, yt; + if (col + 1 > 9 || row < 0 || row > 2) return; + x = COL_XH[col + 1] * 8; + yt = POP_YOFF + 63 * row; + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, yt + 3, x + 31, yt + 64); + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(row, col + 1); + gfx_set_bank(GFX_BANK_NORMAL); +} + /* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — @@ -986,6 +1015,7 @@ typedef struct { int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ int8_t speed; int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ + uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */ } mob_t; static mob_t mobs[MOB_MAX]; @@ -1014,6 +1044,7 @@ void pop_loose_mob_spawn(int row, int col) __banked m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */ m->row = (int8_t)row; m->col = (int8_t)col; + m->room = g_room; /* curmob.room (add_mob, seg007:1010) */ m->speed = 0; m->active = 1; m->clean = 0; @@ -1047,14 +1078,42 @@ void pop_loose_mob_reset(void) __banked } } +/* Смена комнаты. Куски НЕ останавливаем: в оригинале mobs[] чистится только + * в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо + * от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид + * не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12 + * плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только + * heal-историю: новая комната перерисована целиком, старых пикселей нет. */ +void pop_loose_mob_room_changed(void) __banked +{ + uint8_t i; + for (i = 0; i < MOB_MAX; i++) { + mobs[i].defer = 0; + mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; + /* дочистка хвоста относилась к покинутой комнате — она уже не нужна */ + if (!mobs[i].active) mobs[i].clean = 0; + } +} + +/* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки + * и соседей), в чужой — прямо из данных уровня. */ +static uint8_t mob_tile_at(mob_t *m) +{ + if (m->room == g_room) return (uint8_t)(tile_code(m->row, m->col) & 0x1F); + if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1; /* кромка = пол */ + return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F); +} + static void mob_tick_one(mob_t *m, uint8_t pg) { + uint8_t here; if (!m->active && !m->clean) return; + here = (uint8_t)(m->room == g_room); /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; * плита-потолок падает через весь ряд 0, поэтому чистим по фактической * прошлой позиции. */ - if (m->prev_y[pg] != MOB_Y_NONE) { + if (here && m->prev_y[pg] != MOB_Y_NONE) { heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); m->prev_y[pg] = MOB_Y_NONE; } @@ -1067,9 +1126,17 @@ static void mob_tick_one(mob_t *m, uint8_t pg) * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); * пол — приземление (loose_land: debris + do_knock). */ if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { - uint8_t t = (uint8_t)(tile_code(m->row, m->col) & 0x1F); + uint8_t t = mob_tile_at(m); if (t == 0 || t == 11) { m->row++; /* mob_down_a_row */ + } 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; + return; } else if (pop_loose_landed) { /* сигнал прошлого приземления ещё не разобран (один за кадр) — * подержим кусок на границе и приземлим следующим кадром */ @@ -1082,9 +1149,10 @@ static void mob_tick_one(mob_t *m, uint8_t pg) } } if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */ - m->active = 0; m->clean = 2; + m->active = 0; m->clean = (uint8_t)(here ? 2 : 0); return; } + if (!here) return; /* чужая комната — считаем, но не рисуем */ /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто @@ -1165,8 +1233,13 @@ void pop_loose_mob_draw_over(void) __banked uint8_t pop_loose_mob_pos(int *col, int *y) __banked { uint8_t i; + /* check_loose_fall_on_kid (seg007:1192) начинается с Char.room == + * curmob.room: кусок, падающий в покинутой комнате, Киду по голове не + * бьёт. */ for (i = 0; i < MOB_MAX; i++) - if (mobs[i].active) { *col = mobs[i].col; *y = mobs[i].y; return 1; } + if (mobs[i].active && mobs[i].room == g_room) { + *col = mobs[i].col; *y = mobs[i].y; return 1; + } return 0; } diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 0f8445a..f3c631d 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -128,12 +128,22 @@ void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кно * запечкой — створка анимируется (pop_trob animate_leveldoor). */ void pop_leveldoor_redraw(int row, int col) __banked; +/* Ворота ВНУТРИ отрисованной комнаты: перерисовать по живому modif. + * (row,col) — координаты САМИХ ворот; бары рисует ячейка правого соседа + * (draw_tile, ветка lcode==4), туда и идёт wipe+redraw. Порт draw_trob + * (seg007:01E6), которым заканчивается animate_door. */ +void pop_gate_redraw(int row, int col) __banked; + /* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла * (row,col) и покадровое продвижение+отрисовка на back-странице. tick * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ void pop_loose_mob_spawn(int row, int col) __banked; void pop_loose_mob_tick(void) __banked; -void pop_loose_mob_reset(void) __banked; /* остановить кусок (смена комнаты) */ +void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */ + +/* Смена комнаты: куски продолжают падать в СВОИХ комнатах (do_mobs крутит + * mobs[] независимо от drawn_room), сбрасывается только heal-история. */ +void pop_loose_mob_room_changed(void) __banked; /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs, * seg008:1561). Звать после kid_draw, перед fore-слоем. */ diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index 56d3844..ebe3ff4 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -691,6 +691,17 @@ int8_t pop_ctrl_shift_held(void) void pop_ctrl_tick(void) { kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */ + /* control_kid (seg006:0CD1), ПЕРВАЯ строка: HP кончились — персонаж + * мёртв. Без этой ветки смерть от меча стража не доводилась до конца: + * hurt_by_sword играл seq_85, hitp_curr падал в 0, но Kid.alive + * оставался −1 и pop_kid_dead не взводился — Кид досматривал сиквенс, + * возвращался в стойку с нулём HP, и страж убивал его снова по кругу + * (BUG-DEATH-1). pop_kid_dead — наш сигнал главному циклу на респавн + * (в оригинале это счётчик Char.alive в play_kid, seg006:0BEE). */ + if (Kid.alive < 0 && hitp_curr == 0) { + Kid.alive = 0; + pop_kid_dead = 1; + } rest_ctrl_1(); read_input(); read_user_control(); diff --git a/applications/PoP/roomtest/pop_gdraw.c b/applications/PoP/roomtest/pop_gdraw.c index 1946be5..921857a 100644 --- a/applications/PoP/roomtest/pop_gdraw.c +++ b/applications/PoP/roomtest/pop_gdraw.c @@ -39,6 +39,14 @@ static int sx_l[2], sy_l[2]; static uint16_t sw_l[2], sh_l[2]; static uint8_t svalid[2]; +/* Ширина спрайта последнего кадра стража — нужна set_char_collision + * (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам: + * это свойство кадра, а не буфера. Отставание на кадр допустимо — так же + * работает kid_fp_width() у Кида. */ +static uint16_t g_last_w; + +uint16_t pop_guard_fp_width(void) __banked { return g_last_w; } + int pop_guard_load(void) __banked { static const char *const names[GUARD_PAGES] = { @@ -112,6 +120,7 @@ void pop_guard_draw(void) __banked gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0); gfx_set_bank(GFX_BANK_NORMAL); gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1; + g_last_w = w; /* для char_width_half, см. выше */ clip_x = bx; clip_y = top; clip_w = w; clip_h = h; } /* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable, diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index bb25057..76d896e 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -114,6 +114,16 @@ void pop_guard_leave(void) (uint8_t)(Guard.alive >= 0)); } +/* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа — + * floor((xpos − 7 − 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */ +static int8_t guard_col_from_x(uint8_t xpos) +{ + int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN]; + int16_t col = x / TILE_SIZEX; + if (x % TILE_SIZEX < 0) --col; + return (int8_t)col; +} + void pop_guard_enter(uint8_t room) { uint8_t tile, color, skill; @@ -149,7 +159,18 @@ void pop_guard_enter(uint8_t room) * пересчитывает её из колонки только при загрузке уровня, а дальше * ею владеет leave_guard. Иначе тело при возврате прыгает в центр * тайла. */ - if (saved_seq) Guard.x = pop_guard_state_x(room); + if (saved_seq) Guard.x = pop_guard_state_x(room); + /* ...и КОЛОНКУ пересчитываем из X (enter_guard, seg002:180: + * `Char.curr_col = get_tile_div_mod_m7(Char.x)`). Брать её из + * тайла нельзя: leave_guard сохраняет get_tilepos(0, row), то + * есть колонку 0 всегда. У живого стража это незаметно (X там + * сам считается из колонки), а у ЗАПОМНЕННОГО ТРУПА X + * настоящая, и curr_col оставался нулём при теле, лежащем в + * колонке 2. Тайл трупа шёл в обходе раньше тайла Кида, и Кид + * рисовался ПОВЕРХ тела вместо того, чтобы пройти за ним + * (BUG-DRAWORDER-1). Колонка нужна не только отрисовке — на + * неё смотрят коллизия и check_can_guard_see_kid. */ + Guard.curr_col = guard_col_from_x(Guard.x); pop_loadshad(); if (saved_seq) Char.curr_seq = saved_seq; else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE); diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index d4e72d9..32eecdf 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -147,6 +147,11 @@ void pop_hp_invalidate(void) __banked; void pop_guard_heal(void) __banked; void pop_guard_draw(void) __banked; +/* Ширина спрайта последнего нарисованного кадра стража (char_width_half + * в set_char_collision). Нужна для его ТАЙЛА ОБЪЕКТА — от него зависит + * порядок отрисовки относительно Кида. */ +uint16_t pop_guard_fp_width(void) __banked; + /* Убрать стража со сцены (смена комнаты / инициализация). */ void pop_guard_reset(void); diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 8a0ece6..81aa19a 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -359,6 +359,7 @@ static uint8_t char_dx_forward(int8_t dx) /* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу — * их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */ int8_t kid_cur_dx(void) { return cur_frame.dx; } +int8_t kid_cur_dy(void) { return cur_frame.dy; } uint8_t kid_cur_flags(void) { return cur_frame.flags; } /* seqtbl_offset_char: сменить последовательность Kid (единая точка — @@ -740,3 +741,20 @@ void kid_draw(void) * нулевое окно = fore-проход не трогает экран, что и правильно. */ kid_fore_clip(gfx_get_draw_page() & 1); } + +/* Вернуть окно fore-клипа на прямоугольник ПОСЛЕДНЕГО кадра Kid. + * + * Окно (pop_fore_set_clip) — одно на всех, и его ставит КАЖДЫЙ, кто рисует + * персонажа: kid_draw в конце — своё, pop_guard_draw в конце — своё. Пока + * страж рисовался строго ДО Кида, к моменту pop_fore_over_kid в окне + * оказывался Kid, и всё сходилось само. Как только порядок стал + * переменным (guard_over_kid в roomtest.c — порядок задаёт обход тайлов), + * при «страж поверх» окно оставалось СТРАЖЬИМ и fore-проход Кида отсекался + * целиком: Кид рисовался поверх передних столбов. + * + * Поэтому окно восстанавливается явно перед pop_fore_over_kid, а не + * держится «по счастливому порядку вызовов». */ +void kid_fore_clip_restore(void) +{ + kid_fore_clip(gfx_get_draw_page() & 1); +} diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index 75ceaa6..5d97eef 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -100,6 +100,7 @@ void pop_guard_play(void); /* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования * таблицы во втором TU). */ int8_t kid_cur_dx(void); +int8_t kid_cur_dy(void); uint8_t kid_cur_flags(void); /* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая @@ -118,6 +119,11 @@ void pop_kid_set_render_dx(int dx); * теневой копии; запоминает прямоугольник для kid_heal). */ void kid_draw(void); +/* Вернуть окно fore-клипа на прямоугольник последнего кадра Kid. Звать + * ПЕРЕД pop_fore_over_kid, если между ним и kid_draw кто-то ещё рисовал + * персонажа (pop_guard_draw ставит окно по стражу). */ +void kid_fore_clip_restore(void); + /* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid — * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ void kid_draw_splash(void); diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c index d29aa34..05f243b 100644 --- a/applications/PoP/roomtest/pop_level.c +++ b/applications/PoP/roomtest/pop_level.c @@ -16,6 +16,12 @@ extern void _gfx_w0_isr(void); #define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */ +/* Эталонная копия foretable В ТОЙ ЖЕ странице: страница 16 КБ, данных + * уровня 2.3 КБ — место даром. Нужна, потому что foretable теперь + * МУТАБЕЛЬНАЯ (pop_level_set_tile), а рестарт уровня в оригинале — + * это load_level() заново (play_level, seg003:57), то есть все плиты целы + * и зелья на месте. pop_level_reset_tiles восстанавливает из неё. */ +#define LVL_PRISTINE_OFF 0x1000u /* Смещения блоков blueprnt относительно начала данных (Table 6). */ #define BP_FG 0u @@ -74,6 +80,9 @@ int pop_level_load(const char *path) dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i]; dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i]; } + /* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */ + for (i = 0; i < 24u * ROOM_TILES; i++) + pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i]; } sprinter_page_w3(saved_w3); close(fd); @@ -184,6 +193,48 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col) return res; } +/* set_tile-запись в ЖИВУЮ foretable уровня (порт curr_room_tiles[…] = …). + * В оригинале уровень — обычная структура в ОЗУ, и do_pickup (seg006:1671), + * remove_loose (seg007:0EB8), loose_land (seg007:11E8) правят её насовсем. + * У нас уровень в EMM-странице — пишем прямо туда, поэтому изменение + * переживает выход из комнаты (BUG-LVLSTATE-1). Пишем БАЙТ ЦЕЛИКОМ: + * старшие биты старого тайла (напр. 0x20 «solid loose») к новому типу + * отношения не имеют. */ +void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile) +{ + if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return; + gfx_w0_map(lvl_page); + ((uint8_t *)(LVL_DATA_OFF + BP_FG))[(uint16_t)(room - 1) * ROOM_TILES + tilepos] + = tile; + gfx_w0_unmap(); +} + +/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть + * foretable в исходное состояние из эталонной копии. */ +void pop_level_reset_tiles(void) +{ + uint16_t i; + uint8_t *dst; + const uint8_t *src; + if (!lvl_ok) return; + gfx_w0_map(lvl_page); + dst = (uint8_t *)(LVL_DATA_OFF + BP_FG); + src = (const uint8_t *)LVL_PRISTINE_OFF; + for (i = 0; i < 24u * ROOM_TILES; i++) dst[i] = src[i]; + gfx_w0_unmap(); +} + +/* Рестарт уровня, часть вторая (тот же load_level + pos_guards, seg003:83): + * стражи — такая же часть данных уровня, как тайлы, и перезагрузка файла + * возвращает их всех. Убитый до смерти Кида страж после respawn снова жив + * и стоит на исходном тайле; забывается и запомненная поза трупа + * (guards_seq_hi = 0 -> «поднять стандартной стойкой»). */ +void pop_level_reset_guards(void) +{ + if (!lvl_ok) return; + gstate_init(); +} + uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; } uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; } void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; } diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index c46ff49..ace23fa 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -49,6 +49,20 @@ uint8_t pop_room_link(uint8_t room, uint8_t side); * тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */ uint8_t pop_level_tile(uint8_t room, uint8_t tilepos); +/* Записать тайл в ЖИВУЮ foretable уровня (порт curr_room_tiles[tp] = tile: + * do_pickup / remove_loose / loose_land). Изменение персистентно — уровень + * лежит в ОЗУ-странице, а не в ПЗУ, — то есть переживает выход из комнаты, + * как в оригинале. Отменяется только pop_level_reset_tiles. */ +void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile); + +/* Рестарт уровня: вернуть ВСЕ тайлы в исходное (порт load_level, который + * play_level зовёт на каждой итерации — в т.ч. после смерти Кида). */ +void pop_level_reset_tiles(void); + +/* Рестарт уровня: вернуть ВСЕХ стражей (тот же load_level + pos_guards). + * Убитый страж после respawn снова жив — как в оригинале. */ +void pop_level_reset_guards(void); + /* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений). * Между begin/end — только pop_level_tile_raw, без рисования. */ void pop_level_access_begin(void); diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index c46430a..099debc 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -127,12 +127,10 @@ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */ -/* Состояние edge-триггера бампа (порт prev_coll_flags, seg004): колонка, для - * которой сохранены флаги перекрытия, и сами флаги (бит0 = левая грань стены, - * бит1 = правая). BUMP_COL_NONE — данных нет (смена комнаты/старт). */ -#define BUMP_COL_NONE ((int8_t)-128) -static int8_t bump_prev_col; -static uint8_t bump_prev_flags; +/* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам + * (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом + * с check_collisions. Сброс при смене комнаты — coll_prev_row. */ +static void pop_coll_invalidate(void); /* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого * соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично * (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах — @@ -183,7 +181,7 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, /* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия * недействительны (в оригинале prev_coll_room != curr_row_coll_room и * бампа на кадре входа нет). */ - bump_prev_col = BUMP_COL_NONE; + pop_coll_invalidate(); } static uint8_t get_tile(int8_t col, int8_t row) @@ -874,6 +872,13 @@ static void do_pickup(int8_t obj_type, uint8_t tilepos) { pickup_obj_type = obj_type; g_fg[tilepos] = TILE_FLOOR; + /* curr_room_modif[curr_tilepos] = 0 (seg006:1676): у зелья в модификаторе + * лежал тип+фаза пузырька, у меча — счётчик блеска. Тайл стал полом — + * мусор в модификаторе не нужен (в исходное его вернёт только рестарт). */ + { + uint8_t *m = pop_trob_modif(g_room); + if (m) m[tilepos] = 0; + } pop_item_taken = (uint8_t)(tilepos + 1); } @@ -968,7 +973,19 @@ static void check_action(void) { uint8_t action = Kid.action, frame = Kid.frame; if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) { - if (frame == 109 || (frame >= 110 && frame <= 119)) + /* ТОЛЬКО кадр 109 (присед) — как в ванильном seg006:909. Кадры + * вставания 110..119 сюда добавляет ОПЦИОНАЛЬНЫЙ FIX_STAND_ON_THIN_AIR, + * и он состоит из ТРЁХ частей: (1) эта ветка, (2) отмена падения, + * если в 2 px позади ещё пол (seg006:1079), (3) ПРАВКА seqtbl — + * dx(1)→dx(0) и dx(−4)→dx(−3) в standup. У нас seqtbl извлечён из + * данных оригинала, третью часть не применить, а без неё вставание + * уносит весовую точку на 4 px вперёд — в стену. Именно половинчатый + * порт этого фикса ронял Кида сквозь пол: комната 5, приземление с + * уроном на щебень (2,7) у стены (2,8), вставание → колонка 8 = стена + * → start_fall → выпадение в комнату 6 (BUG-STANDUP-1). Ванильное + * поведение (кадр 109) от этого свободно; цена — «встать на воздухе» + * у самой кромки, ровно как в оригинале 1989 года. */ + if (frame == 109) check_on_floor(); } else if (action == ACT_FREEFALL) { do_fall(); @@ -1014,7 +1031,23 @@ static void bumped_floor(void) return; } Kid.fall_y = 0; - if (Kid.action == ACT_BUMPED) return; /* уже в отскоке — не рестартить */ + /* `if (Char.alive)` (seg004:0520) — у ЖИВОГО (alive = −1) сиквенс удара + * ставится ВСЕГДА, даже если персонаж уже в отскоке. Раньше здесь стоял + * наш guard `if (Kid.action == ACT_BUMPED) return;` («не рестартить») — и + * он был корнем BUG-STANDUP-1: + * 1) вставание из приседа (medland) двигает Кида на 1 px в стену — + * срабатывает бамп; + * 2) bumped_floor ПРИЖИМАЕТ y к полу (181), отменяя dy(−2), который + * medland сделал в начале; + * 3) наш guard возвращает управление, medland продолжается и делает + * свои dy(+1), dy(+1) — теперь уже ОТ уровня пола, то есть Кид + * оказывается НИЖЕ пола (y = 182); + * 4) следующий бамп видит «персонаж ниже пола» (беззнаковая разность + * заворачивается) и уводит его в bumped_fall → seq_45 → выпадение + * в комнату снизу. + * У оригинала шага (3) нет: seq_47 обрывает medland, и лишних dy не + * случается. */ + if (!Kid.alive) return; /* труп — только выравнивание */ frame = Kid.frame; /* Кадры прыжков и начала падения — «жёсткий» удар с приседанием * (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */ @@ -1026,104 +1059,202 @@ static void bumped_floor(void) pop_kid_play(); } -/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять - * к грани и отдать исход в bumped_floor/bumped_fall (порт bumped, seg004). - * Это даёт «шаг назад со звуком» при беге/шаге в стену вместо падения. Уже - * в отскоке (action bumped) — только выравниваем, без рестарта сиквенса. - * В падении/висе не трогаем. */ +/* ---- check_collisions (seg004:0004) -------------------------------- * + * ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка + * удара выбирается обходом 9→0 по переходу флага 0→1. + * + * ПОЧЕМУ НЕ «колонка переднего края» (как было до 2026-08-03, BUG-COLL-1): + * колонка края попадает на стену лишь пока край внутри её габаритного окна, + * а окно узкое. Для решётки в col9 (x_bump[14]=184, coll_left=191, левая + * грань 191+10=201) это ровно 201..204 — четыре пикселя. Беговой кадр + * сдвигает край больше чем на четыре, край перескакивает с ≤200 сразу на + * ≥205 (это уже колонка 10, соседняя комната, не стена) — бампа нет, а + * check_leave тут же видит char_x_right ≥ 201 и уводит Кида СКВОЗЬ + * ЗАКРЫТУЮ РЕШЁТКУ. Флаг колонки 9 в оригинале взводится при любом + * перелёте, потому что сравнивается не колонка края, а КООРДИНАТЫ: + * 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены») + * 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань») + * Тем же корнем лечится влёт в стену на длинном прыжке (комн. 6) и провал + * сквозь пол после приземления у стены (комн. 5). + * + * ОТЛИЧИЕ ОТ ОРИГИНАЛА (сознательное): он держит флаги ТРЁХ рядов (curr/ + * above/below), чтобы при смене ряда «прошлые» флаги были настоящими. У + * нас ряд один: сменился ряд — считаем, что персонаж уже перекрывал всё + * (prev = 3), то есть на кадре смены ряда бампа нет, а на следующем он + * посчитается честно. Три ряда — это втрое больше get_tile каждый кадр; + * если где-то вылезет пропущенный бамп ровно на кадре приземления — + * вернуться сюда. */ +/* Колонки считаем от −2 до 11, а не 0..9: колонки за швом — это тайлы + * СОСЕДНЕЙ комнаты (get_tile резолвит их через g_lcol/g_rcol), и решётка + * соседа обязана бампить так же, как своя (иначе Kid уходит сквозь + * закрытые ворота шва — старый BUG-SEAM-PINGPONG). Оригинал делает то же + * самое: get_row_collision_data идёт от col(char_x_left)−1 до + * col(char_x_right)+2 и резолвит комнату через find_room_of_tile, а + * координата грани (coll_tile_left_xpos) остаётся в системе ОТРИСОВАННОЙ + * комнаты — как у нас pop_x_bump[col + FIRST_ONSCREEN_COLUMN]. */ +#define COLL_C0 (-2) /* первая считаемая колонка */ +#define COLL_N 14 /* колонки −2..11 */ +#define COLL_IDX(c) ((c) - COLL_C0) +#define COLL_ROW_NONE ((int8_t)-128) +static uint8_t coll_prev[COLL_N], coll_curr[COLL_N]; +static int8_t coll_prev_row = COLL_ROW_NONE; /* ряд, к которому относится coll_prev */ +/* «нет колонки»: НЕ −1, потому что −1/−2 — законные колонки шва. */ +#define BUMP_NONE ((int8_t)-128) +static int8_t bump_col_l; /* bump_col_left_of_wall (Kid лицом ВПРАВО) */ +static int8_t bump_col_r; /* bump_col_right_of_wall (Kid лицом ВЛЕВО) */ +static int coll_xl, coll_xr; /* char_x_left_coll / char_x_right_coll */ + +/* Смена комнаты: колонки перепроецированы, прошлые флаги недействительны. + * В оригинале это несовпадение prev_coll_room и curr_row_coll_room — + * на кадре входа бампа нет. */ +static void pop_coll_invalidate(void) { coll_prev_row = COLL_ROW_NONE; } + +/* set_char_collision (seg006:0723), горизонтальная часть: габарит персонажа + * в комнатных координатах. char_x_left = obj_x/2 + 58 (минус полуширина при + * взгляде вправо) — это ровно наш char_x_forward_edge для переднего края. */ +static void set_char_collision(void) +{ + int e = char_x_forward_edge(); + int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */ + if (Kid.direction < 0) { coll_xl = e; coll_xr = e + wh; } + else { coll_xr = e; coll_xl = e - wh; } + if (kid_cur_flags() & FRAME_THIN) { coll_xl += 4; coll_xr -= 4; } +} + +/* get_row_collision_data (seg004:0185) для одного ряда: бит0 = «персонаж + * достал левую грань стены колонки», бит1 = «правую». Оригинал считает + * колонки от col(char_x_left)−1 до col(char_x_right)+2 и оставляет прочие + * как есть; мы считаем все четырнадцать — они дешёвые (индекс в массив + + * switch), зато не остаётся протухших ячеек (FIX_COLL_FLAGS SDLPoP). */ +static void get_row_collision_data(int8_t row, uint8_t *flags) +{ + int8_t col; + for (col = COLL_C0; col < COLL_C0 + COLL_N; col++) { + uint8_t f = 0; + uint8_t wt = wall_type(get_tile(col, row)); + if (wt) { + int coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + if (wall_dl[wt] + coll_left < coll_xr) f |= 1; + if (coll_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2; + } + flags[COLL_IDX(col)] = f; + } +} + +static void check_collisions(void) +{ + int8_t col; + bump_col_l = bump_col_r = BUMP_NONE; + /* seg004:0044 — на кадрах разворота коллизии не считаются вовсе. */ + if (Kid.action == ACT_TURN) return; + /* Вне рядов комнаты (падение мимо пола) get_tile отдаёт WALL во всех + * колонках — считать по нему нечего. Стену в падении держит + * do_fall/in_wall, выход из комнаты — check_leave_below. */ + if (Kid.curr_row < 0 || Kid.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; } + + set_char_collision(); + /* move_coll_to_prev (seg004:00DF) в нашем однорядном варианте. */ + if (coll_prev_row == Kid.curr_row) { + for (col = 0; col < COLL_N; col++) coll_prev[col] = coll_curr[col]; + } else { + for (col = 0; col < COLL_N; col++) coll_prev[col] = 3; /* «уже перекрывал» */ + } + coll_prev_row = Kid.curr_row; + get_row_collision_data(Kid.curr_row, coll_curr); + /* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка, + * в которой флаг перешёл 0→1. */ + for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --col) { + uint8_t p = coll_prev[COLL_IDX(col)], c = coll_curr[COLL_IDX(col)]; + if (!(p & 1) && (c & 1)) bump_col_l = col; + if (!(p & 2) && (c & 2)) bump_col_r = col; + } +} + +/* is_obstacle (seg004:037E): препятствие ли тайл колонки на самом деле. + * Побочно оставляет тайл в g_curr_tile (как curr_tile2 у оригинала). */ +static uint8_t is_obstacle_at_col(int8_t col) +{ + uint8_t t = get_tile(col, Kid.curr_row); + if (t == TILE_POTION) return 0; /* зелье не бампит */ + if (t == TILE_GATE) return (uint8_t)!gate_passable(col, Kid.curr_row); + if (t == TILE_CHOMPER) /* закрыт только при modif==2 */ + return (uint8_t)(col >= 0 && col <= 9 && + pop_trob_modif(g_room)[Kid.curr_row * 10 + col] == 2); + /* ветка «прыжок сквозь зеркало» (seg004:239) — уровень 4, не портируем */ + return 1; +} + +/* bumped (seg004:0448). delta_x — сдвиг персонажа В КООРДИНАТАХ (не + * «вперёд»), push_dir — куда толкает: <0 влево, >=0 вправо. Чем кончится + * удар, решает тайл, на котором персонаж оказался после отжатия: сквозь + * стену/верх двери он стоит на СОСЕДНЕЙ клетке (в сторону толчка), для + * прочих препятствий (ворота, зеркало, кусаки) — на клетке препятствия. */ +static void bumped(int delta_x, int8_t push_dir, int8_t tcol) +{ + uint8_t ct; + if (Kid.alive >= 0) return; /* труп не бампит */ + Kid.x = (uint8_t)((int)Kid.x + delta_x); + determine_col(); + ct = g_curr_tile; /* тайл препятствия */ + if (push_dir < 0) { /* толчок ВЛЕВО */ + if (ct == TILE_WALL) + ct = get_tile((int8_t)(tcol - 1), Kid.curr_row); + } else { /* толчок ВПРАВО */ + if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL) + ct = get_tile((int8_t)(tcol + 1), Kid.curr_row); + } + if (tile_is_floor(ct)) bumped_floor(); + else bumped_fall(); + determine_col(); +} + +/* check_bumped_look_right (seg004:030A): Kid смотрит ВПРАВО и достал ЛЕВУЮ + * грань стены — отжать влево. */ +static void check_bumped_look_right(void) +{ + int8_t col = bump_col_l; + uint8_t wt; + int coll_left; + if (Kid.direction != 0) return; /* (меча в руке у нас нет) */ + if (!is_obstacle_at_col(col)) return; + wt = wall_type(g_curr_tile); + if (!wt) return; + coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + bumped(wall_dl[wt] + coll_left - coll_xr, -1, col); +} + +/* check_bumped_look_left (seg004:02D2): Kid смотрит ВЛЕВО и достал ПРАВУЮ + * грань стены — отжать вправо. */ +static void check_bumped_look_left(void) +{ + int8_t col = bump_col_r; + uint8_t wt; + int coll_left; + if (Kid.direction >= 0) return; + if (!is_obstacle_at_col(col)) return; + wt = wall_type(g_curr_tile); + if (!wt) return; + coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + bumped(coll_left - wall_dr[wt] + TILE_RIGHTX - coll_xl, 0, col); +} + +/* check_bumped (seg004:029D). */ static void check_bumped(void) { - uint8_t t, wt; int8_t tcol; int d; - /* Не бампим в падении/висе/подтягивании (seg004 check_bumped guard'ы: - * action hang_climb/hang_straight + кадры подъёма 135..148 — иначе - * climb-up у стены прерывается отскоком). FREEFALL — наш guard: стену - * в падении держит do_fall/in_wall. */ - if (Kid.action == ACT_FREEFALL || Kid.action == ACT_HANGCLIMB || - Kid.action == ACT_HANGSTRAIGHT || Kid.action == ACT_MIDAIR) return; + /* Гарды РОВНО как в оригинале: вис и подтягивание. ПАДЕНИЕ И ПОЛЁТ НЕ + * ИСКЛЮЧЕНЫ — bumped_fall (seg004:04E4) специально разбирает случай + * actions_4_in_freefall (гасит fall_x и отжимает на 4). Наш прежний + * guard `action == FREEFALL || MIDAIR -> return` и был вторым корнем + * BUG-COLL-1: длинный прыжок в стену (комната 6, между (0,6) и (0,7)) + * ничем не останавливался, и Кид оказывался ВНУТРИ кладки. */ + if (Kid.action == ACT_HANGCLIMB || Kid.action == ACT_HANGSTRAIGHT) return; if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */ /* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb), * т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку * hang->hang_fall->seq_11 (правильное приземление). */ if (Kid.frame >= 87 && Kid.frame < 100) return; - /* Вне рядов комнаты (падение мимо пола): get_tile даёт WALL, а - * dist_from_wall_forward/pop_x_bump[] — мусор с большим отриц. сдвигом → - * underflow X и уход в стену. fell_out ловит do_fall. */ - if (Kid.curr_row < 0 || Kid.curr_row > 2) return; - /* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются - * (bump_col_* сбрасываются и функция выходит) — bump'а в развороте нет. */ - if (Kid.action == ACT_TURN) return; - /* Стена ищется по колонке, в которую упирается ПЕРЕДНИЙ КРАЙ персонажа - * (порт seg004: bump_col_left/right_of_wall — колонка, где край пересёк - * грань; НЕ curr_col!). Через curr_col было ДВА расхождения: (1) весовая - * точка кадра заезжает за грань раньше края, curr_col становился −1 на - * шве, ворота оказывались «под персонажем» и гард односторонней стены их - * снимал — Kid уходил в соседнюю комнату СКВОЗЬ ОПУЩЕННУЮ РЕШЁТКУ; - * (2) тот же гард был нужен только из-за отсутствия проверки разворота - * (см. выше). Колонка переднего края даёт и то, и другое даром: стоя - * СПРАВА от ворот лицом вправо, край в соседней колонке — ворота не - * бампят; заехав краем за их грань лицом влево — бампят. */ - tcol = get_tile_div_mod_m7(char_front_coll()); - t = get_tile(tcol, Kid.curr_row); - wt = wall_type(t); - /* EDGE-триггер (порт check_collisions + get_row_collision_data, seg004:0004). - * Оригинал считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ направления: - * 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены») - * 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань») - * и зовёт bumped() ТОЛЬКО когда нужный флаг перешёл 0 -> 1 (взгляд вправо - * смотрит на 0x0F, влево — на 0xF0). Без этого проверка «уровнем» ловит - * ЛЮБОЕ перекрытие: Kid, стоящий В проёме опущенной решётки, при развороте - * на месте получал ложный бамп и выпихивался сквозь неё в соседнюю - * комнату. С флагами от габарита разворот их не меняет — бампа нет. */ - { - uint8_t f = 0, prev, need; - if (wt) { - int coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; - int lw = wall_dl[wt] + coll_left; /* get_left_wall_xpos */ - int rw = coll_left - wall_dr[wt] + TILE_RIGHTX; /* get_right_wall_xpos */ - int e = char_x_forward_edge(), wh = ((int)kid_fp_width() + 1) >> 1; - int xl, xr; - if (Kid.direction < 0) { xl = e; xr = e + wh; } /* габарит персонажа */ - else { xr = e; xl = e - wh; } - if (kid_cur_flags() & FRAME_THIN) { xl += 4; xr -= 4; } - if (lw < xr) f |= 1; - if (rw > xl) f |= 2; - } - /* прошлый кадр: та же колонка — её флаги; другая — считаем, что не - * перекрывали; «нет данных» (смена комнаты) — считаем, что перекрывали - * всё, чтобы на кадре входа бампа не было (в оригинале это даёт - * несовпадение prev_coll_room и curr_row_coll_room). */ - prev = (bump_prev_col == BUMP_COL_NONE) ? 3u - : (bump_prev_col == tcol ? bump_prev_flags : 0u); - bump_prev_col = tcol; - bump_prev_flags = f; - if (!wt) return; - need = (Kid.direction < 0) ? 2u : 1u; - if (!(f & need) || (prev & need)) return; /* не перекрыл / уже перекрывал */ - } - if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row)) - return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */ - d = dist_from_wall_forward(t, tcol); - if (d >= 0) return; /* ещё не зашёл за грань */ - Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */ - determine_col(); - /* Хвост bumped (seg004:04E4): чем КОНЧИТСЯ удар, решает тайл, на котором - * персонаж оказался после отжатия от грани. Сквозь стену/верх двери он - * стоит на СОСЕДНЕЙ клетке (в сторону толчка), для прочих препятствий - * (ворота, зеркало, кусаки) — на клетке самого препятствия (у ворот пол - * есть, поэтому tile_is_floor их считает полом). */ - { - uint8_t ct = t; - if (Kid.direction < 0) { /* смотрит влево => толчок ВПРАВО */ - if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL) - ct = get_tile((int8_t)(tcol + 1), Kid.curr_row); - } else { /* смотрит вправо => толчок ВЛЕВО */ - if (ct == TILE_WALL) - ct = get_tile((int8_t)(tcol - 1), Kid.curr_row); - } - if (tile_is_floor(ct)) bumped_floor(); - else bumped_fall(); - } - determine_col(); + if (bump_col_l != BUMP_NONE) check_bumped_look_right(); + else if (bump_col_r != BUMP_NONE) check_bumped_look_left(); } /* ---- Loose floors (seg007) ---------------------------------------- * @@ -1227,7 +1358,8 @@ void pop_check_knock(void) __banked /* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest * индексируются позицией тайла и не должны утечь из старой комнаты в новую. - * Плюс останавливает падающий кусок (pop_bg). */ + * Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и + * должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */ void pop_loose_reset(void) __banked { int i; @@ -1240,7 +1372,7 @@ void pop_loose_reset(void) __banked pop_ceil_fell = 0; pop_debris_at = 0; pop_loose_landed = 0; - pop_loose_mob_reset(); + pop_loose_mob_room_changed(); } /* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге @@ -1615,6 +1747,7 @@ void pop_phys_tick(void) __banked fall_accel(); fall_speed(); determine_col(); + check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */ check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_action(); check_press(); /* seg006: стойка/пробой loose → make_loose_fall */ diff --git a/applications/PoP/roomtest/pop_redraw.c b/applications/PoP/roomtest/pop_redraw.c index 45af5fa..b534739 100644 --- a/applications/PoP/roomtest/pop_redraw.c +++ b/applications/PoP/roomtest/pop_redraw.c @@ -59,6 +59,7 @@ void pop_redraw_needed(void) case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break; case POP_RD_FLOOR: pop_floor_bake(row, col); break; case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break; + case POP_RD_GATE: pop_gate_redraw(row, col); break; default: break; } if (--rd_cnt[i]) left++; diff --git a/applications/PoP/roomtest/pop_redraw.h b/applications/PoP/roomtest/pop_redraw.h index cdad683..b94846b 100644 --- a/applications/PoP/roomtest/pop_redraw.h +++ b/applications/PoP/roomtest/pop_redraw.h @@ -29,6 +29,7 @@ #define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */ #define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */ #define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */ +#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */ /* Виды перерисовки полосы у потолка (по колонке). */ #define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */ diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 86b3f30..f741df7 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -460,6 +460,15 @@ void pop_process_trobs(uint8_t cur_room) * страницы: живой trob метит каждый кадр по одной. */ pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1)); } + if (room == cur_room && code == TILE_GATE) { + /* draw_trob (seg007:01E6), которым заканчивается animate_door: + * пока trob жив — решётка едет каждый кадр, помечаем ТЕКУЩУЮ + * страницу; последний кадр (type<0, ворота встали) — ОБЕ, иначе + * на второй странице дабл-буфера застынет предыдущее положение + * и решётка дрожит через кадр. Тайлпос — САМИ ворота, соседа + * справа выбирает pop_gate_redraw. */ + pop_set_redraw(tp, POP_RD_GATE, (uint8_t)(type < 0 ? 2 : 1)); + } if (room == cur_room && code == TILE_LEVELDOOR) { /* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там * её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 87bebee..786110b 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -16,6 +16,7 @@ #include "pop_level.h" #include "pop_trob.h" #include "pop_redraw.h" +#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */ #include "pop_guard.h" #include "pop_cheat.h" @@ -130,6 +131,89 @@ static void update_kid_render_dx(void) pop_kid_set_render_dx(dx); } +/* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) ----------------------- + * + * В оригинале никто не «поверх» по определению: и Кид, и страж попадают в + * midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а + * тайлы обходятся в фиксированном порядке — redraw_needed_tiles (seg008:1B06) + * идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ — тот + * поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или + * выше, его тайл в обходе позже. Совпали тайлы — решает sort_curr_objs + * (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже. + * + * Мы рисовали страж→Кид всегда, то есть Кид был поверх безусловно. + * + * Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 → (2 - row). */ +#define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col)) + +/* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */ +static int8_t tile_div_mod(int x) +{ + int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */ + int c = v / TILE_SIZEX; + if (v % TILE_SIZEX < 0) --c; + return (int8_t)c; +} + +/* char_x_left из set_char_collision (seg006:0723) — левая граница габарита. + * ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к + * *_coll, а колонки char_col_left/right считает по этой. */ +static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx, + uint8_t flags, uint16_t w) +{ + int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */ + uint8_t fl = flags; + if (dir < 0) fl ^= 0x80; + /* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */ + if (!(fl & 0x80) && e <= 57) e++; + if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */ + return e; +} + +/* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа. + * + * Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил + * сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ + * колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому + * бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров + * бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих: + * enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт + * дал бы перекос в другую сторону. */ +static void objtile_at_char(int8_t *row, int8_t *col, + const pop_char_t *ch, int8_t dx, int8_t dy, + uint8_t flags, uint16_t w) +{ + int8_t r = ch->curr_row, c = ch->curr_col; + if (ch->action == 1) { /* actions_1_run_jump */ + int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy)); + if (br == -1) br = 3; /* char_bottom_row */ + r = br; + c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w)); + if (c < 0) c = 0; /* MAX(char_col_left, 0) */ + } + if ((ch->frame >= 135 && ch->frame < 149) || + ch->action == 2 || ch->action == 3 || + ch->action == 4 || ch->action == 6) c--; + *row = r; *col = c; +} + +/* 1 = страж рисуется ПОСЛЕ Кида (поверх). */ +static uint8_t guard_over_kid(void) +{ + int8_t kr, kc, gr, gc; + int kk, gk; + if (Guard.charid == 0 || Guard.room != cur_room) return 0; + objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(), + kid_cur_flags(), kid_fp_width()); + objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy, + pop_gframe.flags, pop_guard_fp_width()); + kk = OBJ_ORDER_KEY(kr, kc); + gk = OBJ_ORDER_KEY(gr, gc); + if (gk != kk) return (uint8_t)(gk > kk); + /* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */ + return (uint8_t)((int)Guard.y + pop_gframe.dy > kid_fp_obj_y()); +} + /* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый * draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта * реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен @@ -143,17 +227,15 @@ static uint8_t seam_redraw; /* оставшиеся кадры редроя ( static uint8_t room_label_sig = 0xFF; static uint8_t room_label_redraw; -/* Персистентные переопределения тайлов уровня: упавший loose-кусок оставил - * щебень (floor → debris), а на месте самой плиты — пусто (в т.ч. в комнате - * СВЕРХУ: провалившийся потолок = колодец). Минимальный per-room override: - * живёт между переходами, применяется в enter_room поверх загруженных тайлов - * (и для ряда 2 верхнего соседа). (Полноценное per-room modifier-состояние — - * в docs/gates_spikes_plan.md, P0.) */ +/* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления + * куска, поднятый предмет → пол) пишутся ПРЯМО В УРОВЕНЬ — + * pop_level_set_tile, как в оригинале (curr_room_tiles[…] = …). Раньше тут + * жила таблица переопределений на 8 записей, и девятая правка молча + * терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в + * комнату (BUG-LVLSTATE-1). */ #define TILE_DEBRIS 0x0E #define TILE_EMPTY 0x00 #define TILE_FLOOR 0x01 -#define OVR_MAX 8 -static uint8_t ovr_room[OVR_MAX], ovr_pos[OVR_MAX], ovr_tile[OVR_MAX], ovr_cnt; /* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам. * flash_on — вспышка фона активна (нулевая запись палитры подменена). */ @@ -169,27 +251,6 @@ static void flash_bg(uint8_t r, uint8_t g, uint8_t b) gfx_pal_set(1, 0, r, g, b); } -static void add_tile_override(uint8_t room, uint8_t pos, uint8_t tile) -{ - uint8_t i; - for (i = 0; i < ovr_cnt; i++) - if (ovr_room[i] == room && ovr_pos[i] == pos) { ovr_tile[i] = tile; return; } - if (ovr_cnt < OVR_MAX) { - ovr_room[ovr_cnt] = room; ovr_pos[ovr_cnt] = pos; - ovr_tile[ovr_cnt] = tile; ovr_cnt++; - } -} - -/* Применить переопределения комнаты room к массиву тайлов: base = tilepos - * первого элемента (0 для полной комнаты, 20 для ряда 2 верхнего соседа). */ -static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t n) -{ - uint8_t i; - for (i = 0; i < ovr_cnt; i++) - if (ovr_room[i] == room && ovr_pos[i] >= base && ovr_pos[i] < base + n) - tiles[ovr_pos[i] - base] = ovr_tile[i]; -} - /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */ @@ -199,7 +260,6 @@ static void enter_room(uint8_t room) pop_guard_leave(); /* запомнить стража уходящей комнаты */ uint8_t *room_mod; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); - apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */ pop_map_set(room_fg); pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), pop_room_link(room, 2), pop_room_link(room, 3), @@ -214,7 +274,6 @@ static void enter_room(uint8_t room) pop_level_access_begin(); for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c)); pop_level_access_end(); - apply_overrides(up, above_fg, 20, 10); /* провалившийся потолок = колодец */ for (c = 0; c < 10; c++) above_mod[c] = um[20 + c]; pop_room_set_above(above_fg, above_mod); pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */ @@ -292,7 +351,15 @@ static void pop_start_level(uint8_t entry_pose) int8_t dir = (int8_t)~pop_level_start_dir(); uint8_t seq = SEQ_TURN; - pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */ + /* Рестарт уровня = load_level() заново (play_level, seg003:57): в + * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы — + * разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и + * убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */ + pop_trob_reset(); + pop_level_reset_tiles(); + pop_level_reset_guards(); + pop_guard_reset(); /* start_level: Guard.direction = none */ + pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы @@ -539,26 +606,26 @@ int main(void) uint8_t fcol = (uint8_t)(fpos % 10); uint8_t down = pop_room_link(cur_room, LINK_DOWN); pop_loose_fell = 0; - add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */ + pop_level_set_tile(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */ if (down) { int8_t lrow = pop_room_col_landing(down, fcol); - if (lrow >= 0) add_tile_override(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS); + if (lrow >= 0) pop_level_set_tile(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS); } } if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */ uint8_t ccol = (uint8_t)(pop_ceil_fell - 1); uint8_t up = pop_room_link(cur_room, 2); pop_ceil_fell = 0; - if (up) add_tile_override(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */ + if (up) pop_level_set_tile(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */ } if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */ - add_tile_override(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS); + pop_level_set_tile(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS); pop_debris_at = 0; } if (pop_item_taken) { /* предмет поднят → тайл стал полом */ item_pos = (uint8_t)(pop_item_taken - 1); item_bake = 2; /* запечь на ОБЕИХ страницах */ - add_tile_override(cur_room, item_pos, TILE_FLOOR); + pop_level_set_tile(cur_room, item_pos, TILE_FLOOR); pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */ pop_item_taken = 0; } @@ -588,9 +655,14 @@ int main(void) } update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ PROF(6); /* спрайты: страж, Kid, fore поверх */ - pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */ - PROF(6); /* kid_draw (тот же циан) */ - kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ + { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль + * персонажа (см. guard_over_kid). */ + uint8_t g_after = guard_over_kid(); + if (!g_after) pop_guard_draw(); + PROF(6); /* kid_draw (тот же циан) */ + kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ + if (g_after) pop_guard_draw(); + } pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */ if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ kid_draw_splash(); @@ -598,6 +670,11 @@ int main(void) } pop_hp_draw(); /* полоса HP — только при изменении */ PROF(6); /* fore поверх Kid + борта */ + /* Окно fore-клипа общее, и pop_guard_draw ставит своё. При «страж + * поверх» он отрабатывает ПОСЛЕ kid_draw, поэтому окно Кида + * восстанавливаем явно — иначе передние грани у Кида отсекаются и + * он рисуется поверх столбов. */ + kid_fore_clip_restore(); pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), kid_fp_width(), kid_fp_height(), Kid.direction, Kid.frame, Kid.action);