L1: фиксы прогона уровня 1 — уровень мутабелен, стражи, loose-плиты, порядок

Одиннадцать наблюдений первого прогона свелись к шести корням, четыре
наблюдения второго — ещё к четырём.  Разбор каждого — bug_closed.md.

Первая волна:
- BUG-LVLSTATE-1: уровень стал мутабельным (эталонная копия foretable для
  рестарта, pop_level_set_tile вместо таблицы оверрайдов);
- BUG-RESPAWN-1: рестарт = load_level, тайлы возвращаются из эталона;
- BUG-DEATH-1: смерть от меча доигрывается (порт control_kid, seg006:0CD1);
- BUG-GATE-ANIM-1: ворота в отрисованной комнате перерисовываются
  (POP_RD_GATE, порт draw_trob seg007:01E6);
- BUG-COLL-1: полный порт check_collisions/bumped (seg004) вместо поиска
  стены только в колонке переднего края;
- BUG-STANDUP-1: убран лишний guard в bumped_floor — вставание у стены
  роняло Кида сквозь пол.

Вторая волна:
- BUG-RESPAWN-2: рестарт возвращает и СТРАЖЕЙ (в оригинале play_level на
  каждой итерации делает load_level + pos_guards);
- BUG-LOOSE-2: падающий кусок привязан к своей комнате и долетает после
  ухода Кида (do_mobs крутит mobs[] независимо от drawn_room);
- BUG-DRAWORDER-1: порядок «Кид / страж» задаётся обходом тайлов
  (redraw_needed_tiles: ряды 2,1,0, колонки 0..9), а не ролью персонажа.

По BUG-DRAWORDER-1 понадобилось три захода, и два первых были неполны:
  1) сам порядок — но общее окно fore-клипа осталось стражьим, и Кид
     нарисовался поверх передних столбов (kid_fore_clip_restore);
  2) enter_guard брал curr_col из тайла, а leave_guard пишет туда
     get_tilepos(0,row) — у запомненного ТРУПА колонка была 0 при
     настоящей X.  Теперь колонка выводится из X, как в оригинале;
  3) ветка actions_1_run_jump в set_objtile_at_char оказалась не
     «упрощаемой»: в беге тайл берётся из нижнего ряда и ЛЕВОЙ колонки
     габарита, поэтому бегущий Кид уходит за объекты справа.  Считается
     для обоих персонажей — enter_guard ставит action=1 и стражу.

Проверено в MAME: зелья/меч/плиты переживают выход из комнаты и
восстанавливаются после смерти; кнопка room5 поднимает решётку; падение с
кнопки больше не роняет в комнату 6; убитый страж жив после respawn;
Кид проходит за телом стража.  make size-check — роста нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Александр Петров
2026-08-03 22:35:41 +03:00
parent 1f16e8fa70
commit b5d2a81ee3
21 changed files with 1070 additions and 164 deletions
+225
View File
@@ -398,3 +398,228 @@ MAME по кромкам.
Следствие: в 13/18/24 возможен «мусор в шве» — наш рендер кромки читает Следствие: в 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 (вечер)
<a id="bug-kbd-4"></a>
## 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 программ, роста нет.
<a id="bug-respawn-2"></a>
## 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.
+178 -16
View File
@@ -1,30 +1,169 @@
# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации # roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней)
odd-pixel арифметика `char_x`, подстановка тайла нажатой кнопки, баг переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
кодогенератора SDCC) переехало в [`bug_closed.md`](bug_closed.md): прежде чем грепни там по симптому.
заводить новый баг, грепни там по симптому.
Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта: Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта:
механику сверять с `../SDLPoP/src/` ДО кодинга. механику сверять с `../SDLPoP/src/` ДО кодинга.
Ревизия списка: **2026-08-01** (задача L1-TRIAGE). Закрыты и перенесены в Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1). Одиннадцать
архив семь записей: BUG-1, BUG-2, BUG-3 (стояли как Critical с 2026-07-21, наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены
прогнаны в MAME) и вся косметика окклюзии BUG-CEIL-1/2/3, BUG-OCCL-1 (была в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень
починена кодом ещё в июле, список просто отстал — чем именно закрыта каждая, стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень),
расписано в [`bug_closed.md`](bug_closed.md)). 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 | что | тип | | ID | что | тип | статус |
|----|-----|-----| |----|-----|-----|--------|
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | | [BUG-DRAWORDER-1](#bug-draworder-1) | Кид рисуется поверх тела стража (в оригинале — за телом) | Minor | фикс есть, **сценарий не проверен** |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | | [BUG-LOOSE-2](#bug-loose-2) | осколки только от одной из двух падающих плит (комната 12) | Minor | фикс есть, **гонка не воспроизведена** |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | | [BUG-SEAM-DRAW-1](#bug-seam-draw-1) | у решётки на шве экран переключается, а Кид в шве НЕ рисуется | Minor | открыт |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |
--- ---
<a id="ручная-перепроверка-2026-08-03"></a>
# Ручная перепроверка фиксов (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
<a id="bug-draworder-1"></a>
## 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 таблицы выше.
<a id="bug-loose-2"></a>
## 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 таблицы выше.
<a id="bug-seam-draw-1"></a>
## 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) # Оптимизация отрисовки (записано 2026-07-29)
@@ -139,3 +278,26 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и - **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в `S`. Все они со временем съедутся в общий блок читов, разрешаемый в
настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`). настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
---
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
## Сырые наблюдения (прогон 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 | закрыт |
+80
View File
@@ -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. но подумать над этим надо.
Пока ничего не предпринимай, только подумай что и как можно сделать.
Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

+77 -4
View File
@@ -922,6 +922,35 @@ void pop_leveldoor_redraw(int row, int col) __banked
gfx_set_bank(GFX_BANK_NORMAL); 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 (dby62 = 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 (текущая страница). Порт /* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт
* redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И
* тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose —
@@ -986,6 +1015,7 @@ typedef struct {
int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */
int8_t speed; int8_t speed;
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
} mob_t; } mob_t;
static mob_t mobs[MOB_MAX]; 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->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
m->row = (int8_t)row; m->row = (int8_t)row;
m->col = (int8_t)col; m->col = (int8_t)col;
m->room = g_room; /* curmob.room (add_mob, seg007:1010) */
m->speed = 0; m->speed = 0;
m->active = 1; m->active = 1;
m->clean = 0; 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) static void mob_tick_one(mob_t *m, uint8_t pg)
{ {
uint8_t here;
if (!m->active && !m->clean) return; if (!m->active && !m->clean) return;
here = (uint8_t)(m->room == g_room);
/* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
* страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2;
* плита-потолок падает через весь ряд 0, поэтому чистим по фактической * плита-потолок падает через весь ряд 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); heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
m->prev_y[pg] = MOB_Y_NONE; 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_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
* пол — приземление (loose_land: debris + do_knock). */ * пол — приземление (loose_land: debris + do_knock). */
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { 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) { if (t == 0 || t == 11) {
m->row++; /* mob_down_a_row */ 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) { } 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) { /* весь ниже поля (улетел из комнаты) */ if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
m->active = 0; m->clean = 2; m->active = 0; m->clean = (uint8_t)(here ? 2 : 0);
return; return;
} }
if (!here) return; /* чужая комната — считаем, но не рисуем */
/* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ
* тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри
* ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто * ОДНОГО тайла сортируются по 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 pop_loose_mob_pos(int *col, int *y) __banked
{ {
uint8_t i; uint8_t i;
/* check_loose_fall_on_kid (seg007:1192) начинается с Char.room ==
* curmob.room: кусок, падающий в покинутой комнате, Киду по голове не
* бьёт. */
for (i = 0; i < MOB_MAX; i++) 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; return 0;
} }
+11 -1
View File
@@ -128,12 +128,22 @@ void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кно
* запечкой — створка анимируется (pop_trob animate_leveldoor). */ * запечкой — створка анимируется (pop_trob animate_leveldoor). */
void pop_leveldoor_redraw(int row, int col) __banked; 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): спавн при падении тайла /* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick * (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col) __banked; void pop_loose_mob_spawn(int row, int col) __banked;
void pop_loose_mob_tick(void) __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, /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после kid_draw, перед fore-слоем. */ * seg008:1561). Звать после kid_draw, перед fore-слоем. */
+11
View File
@@ -691,6 +691,17 @@ int8_t pop_ctrl_shift_held(void)
void pop_ctrl_tick(void) void pop_ctrl_tick(void)
{ {
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */ 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(); rest_ctrl_1();
read_input(); read_input();
read_user_control(); read_user_control();
+9
View File
@@ -39,6 +39,14 @@ static int sx_l[2], sy_l[2];
static uint16_t sw_l[2], sh_l[2]; static uint16_t sw_l[2], sh_l[2];
static uint8_t svalid[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 int pop_guard_load(void) __banked
{ {
static const char *const names[GUARD_PAGES] = { 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_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL); 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; 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; clip_x = bx; clip_y = top; clip_w = w; clip_h = h;
} }
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable, /* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
+22 -1
View File
@@ -114,6 +114,16 @@ void pop_guard_leave(void)
(uint8_t)(Guard.alive >= 0)); (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) void pop_guard_enter(uint8_t room)
{ {
uint8_t tile, color, skill; uint8_t tile, color, skill;
@@ -149,7 +159,18 @@ void pop_guard_enter(uint8_t room)
* пересчитывает её из колонки только при загрузке уровня, а дальше * пересчитывает её из колонки только при загрузке уровня, а дальше
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр * ею владеет 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(); pop_loadshad();
if (saved_seq) Char.curr_seq = saved_seq; if (saved_seq) Char.curr_seq = saved_seq;
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE); else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
+5
View File
@@ -147,6 +147,11 @@ void pop_hp_invalidate(void) __banked;
void pop_guard_heal(void) __banked; void pop_guard_heal(void) __banked;
void pop_guard_draw(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); void pop_guard_reset(void);
+18
View File
@@ -359,6 +359,7 @@ static uint8_t char_dx_forward(int8_t dx)
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу — /* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */ * их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
int8_t kid_cur_dx(void) { return cur_frame.dx; } 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; } uint8_t kid_cur_flags(void) { return cur_frame.flags; }
/* seqtbl_offset_char: сменить последовательность Kid (единая точка — /* seqtbl_offset_char: сменить последовательность Kid (единая точка —
@@ -740,3 +741,20 @@ void kid_draw(void)
* нулевое окно = fore-проход не трогает экран, что и правильно. */ * нулевое окно = fore-проход не трогает экран, что и правильно. */
kid_fore_clip(gfx_get_draw_page() & 1); 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);
}
+6
View File
@@ -100,6 +100,7 @@ void pop_guard_play(void);
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования /* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
* таблицы во втором TU). */ * таблицы во втором TU). */
int8_t kid_cur_dx(void); int8_t kid_cur_dx(void);
int8_t kid_cur_dy(void);
uint8_t kid_cur_flags(void); uint8_t kid_cur_flags(void);
/* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая /* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая
@@ -118,6 +119,11 @@ void pop_kid_set_render_dx(int dx);
* теневой копии; запоминает прямоугольник для kid_heal). */ * теневой копии; запоминает прямоугольник для kid_heal). */
void kid_draw(void); 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 — /* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid —
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
void kid_draw_splash(void); void kid_draw_splash(void);
+51
View File
@@ -16,6 +16,12 @@
extern void _gfx_w0_isr(void); extern void _gfx_w0_isr(void);
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */ #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). */ /* Смещения блоков blueprnt относительно начала данных (Table 6). */
#define BP_FG 0u #define BP_FG 0u
@@ -74,6 +80,9 @@ int pop_level_load(const char *path)
dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i]; dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + 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); sprinter_page_w3(saved_w3);
close(fd); close(fd);
@@ -184,6 +193,48 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col)
return res; 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_doorlink1(uint8_t i) { return dl1[i]; }
uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; } uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; }
void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; } void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }
+14
View File
@@ -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); 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 на много чтений). /* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
* Между begin/end — только pop_level_tile_raw, без рисования. */ * Между begin/end — только pop_level_tile_raw, без рисования. */
void pop_level_access_begin(void); void pop_level_access_begin(void);
+235 -102
View File
@@ -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_curr_tile; /* = curr_tile2: тайл последнего get_tile */
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */ static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
/* Состояние edge-триггера бампа (порт prev_coll_flags, seg004): колонка, для /* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам
* которой сохранены флаги перекрытия, и сами флаги (бит0 = левая грань стены, * (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом
* бит1 = правая). BUMP_COL_NONE — данных нет (смена комнаты/старт). */ * с check_collisions. Сброс при смене комнаты — coll_prev_row. */
#define BUMP_COL_NONE ((int8_t)-128) static void pop_coll_invalidate(void);
static int8_t bump_prev_col;
static uint8_t bump_prev_flags;
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого /* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
* соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично * соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично
* (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах — * (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 и * недействительны (в оригинале 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) 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; pickup_obj_type = obj_type;
g_fg[tilepos] = TILE_FLOOR; 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); pop_item_taken = (uint8_t)(tilepos + 1);
} }
@@ -968,7 +973,19 @@ static void check_action(void)
{ {
uint8_t action = Kid.action, frame = Kid.frame; uint8_t action = Kid.action, frame = Kid.frame;
if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) { 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(); check_on_floor();
} else if (action == ACT_FREEFALL) { } else if (action == ACT_FREEFALL) {
do_fall(); do_fall();
@@ -1014,7 +1031,23 @@ static void bumped_floor(void)
return; return;
} }
Kid.fall_y = 0; 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; frame = Kid.frame;
/* Кадры прыжков и начала падения — «жёсткий» удар с приседанием /* Кадры прыжков и начала падения — «жёсткий» удар с приседанием
* (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */ * (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */
@@ -1026,104 +1059,202 @@ static void bumped_floor(void)
pop_kid_play(); pop_kid_play();
} }
/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять /* ---- check_collisions (seg004:0004) -------------------------------- *
* к грани и отдать исход в bumped_floor/bumped_fall (порт bumped, seg004). * ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка
* Это даёт «шаг назад со звуком» при беге/шаге в стену вместо падения. Уже * удара выбирается обходом 9→0 по переходу флага 0→1.
* в отскоке (action bumped) — только выравниваем, без рестарта сиквенса. *
* В падении/висе не трогаем. */ * ПОЧЕМУ НЕ «колонка переднего края» (как было до 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) static void check_bumped(void)
{ {
uint8_t t, wt; int8_t tcol; int d; /* Гарды РОВНО как в оригинале: вис и подтягивание. ПАДЕНИЕ И ПОЛЁТ НЕ
/* Не бампим в падении/висе/подтягивании (seg004 check_bumped guard'ы: * ИСКЛЮЧЕНЫ — bumped_fall (seg004:04E4) специально разбирает случай
* action hang_climb/hang_straight + кадры подъёма 135..148 — иначе * actions_4_in_freefall (гасит fall_x и отжимает на 4). Наш прежний
* climb-up у стены прерывается отскоком). FREEFALL — наш guard: стену * guard `action == FREEFALL || MIDAIR -> return` и был вторым корнем
* в падении держит do_fall/in_wall. */ * BUG-COLL-1: длинный прыжок в стену (комната 6, между (0,6) и (0,7))
if (Kid.action == ACT_FREEFALL || Kid.action == ACT_HANGCLIMB || * ничем не останавливался, и Кид оказывался ВНУТРИ кладки. */
Kid.action == ACT_HANGSTRAIGHT || Kid.action == ACT_MIDAIR) return; if (Kid.action == ACT_HANGCLIMB || Kid.action == ACT_HANGSTRAIGHT) return;
if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */ if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */
/* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb), /* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb),
* т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку * т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку
* hang->hang_fall->seq_11 (правильное приземление). */ * hang->hang_fall->seq_11 (правильное приземление). */
if (Kid.frame >= 87 && Kid.frame < 100) return; if (Kid.frame >= 87 && Kid.frame < 100) return;
/* Вне рядов комнаты (падение мимо пола): get_tile даёт WALL, а if (bump_col_l != BUMP_NONE) check_bumped_look_right();
* dist_from_wall_forward/pop_x_bump[] — мусор с большим отриц. сдвигом → else if (bump_col_r != BUMP_NONE) check_bumped_look_left();
* 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();
} }
/* ---- Loose floors (seg007) ---------------------------------------- * /* ---- Loose floors (seg007) ---------------------------------------- *
@@ -1227,7 +1358,8 @@ void pop_check_knock(void) __banked
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest /* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
* индексируются позицией тайла и не должны утечь из старой комнаты в новую. * индексируются позицией тайла и не должны утечь из старой комнаты в новую.
* Плюс останавливает падающий кусок (pop_bg). */ * Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и
* должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */
void pop_loose_reset(void) __banked void pop_loose_reset(void) __banked
{ {
int i; int i;
@@ -1240,7 +1372,7 @@ void pop_loose_reset(void) __banked
pop_ceil_fell = 0; pop_ceil_fell = 0;
pop_debris_at = 0; pop_debris_at = 0;
pop_loose_landed = 0; pop_loose_landed = 0;
pop_loose_mob_reset(); pop_loose_mob_room_changed();
} }
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге /* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
@@ -1615,6 +1747,7 @@ void pop_phys_tick(void) __banked
fall_accel(); fall_accel();
fall_speed(); fall_speed();
determine_col(); determine_col();
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action(); check_action();
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */ check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
+1
View File
@@ -59,6 +59,7 @@ void pop_redraw_needed(void)
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break; 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_FLOOR: pop_floor_bake(row, col); break;
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(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; default: break;
} }
if (--rd_cnt[i]) left++; if (--rd_cnt[i]) left++;
+1
View File
@@ -29,6 +29,7 @@
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */ #define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */ #define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */ #define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */
/* Виды перерисовки полосы у потолка (по колонке). */ /* Виды перерисовки полосы у потолка (по колонке). */
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */ #define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
+9
View File
@@ -460,6 +460,15 @@ void pop_process_trobs(uint8_t cur_room)
* страницы: живой trob метит каждый кадр по одной. */ * страницы: живой trob метит каждый кадр по одной. */
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1)); 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) { if (room == cur_room && code == TILE_LEVELDOOR) {
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там /* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе * её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
+117 -40
View File
@@ -16,6 +16,7 @@
#include "pop_level.h" #include "pop_level.h"
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_redraw.h" #include "pop_redraw.h"
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
#include "pop_guard.h" #include "pop_guard.h"
#include "pop_cheat.h" #include "pop_cheat.h"
@@ -130,6 +131,89 @@ static void update_kid_render_dx(void)
pop_kid_set_render_dx(dx); 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): тяжёлый /* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта * draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен * реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
@@ -143,17 +227,15 @@ static uint8_t seam_redraw; /* оставшиеся кадры редроя (
static uint8_t room_label_sig = 0xFF; static uint8_t room_label_sig = 0xFF;
static uint8_t room_label_redraw; static uint8_t room_label_redraw;
/* Персистентные переопределения тайлов уровня: упавший loose-кусок оставил /* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления
* щебень (floor → debris), а на месте самой плиты — пусто (в т.ч. в комнате * куска, поднятый предмет → пол) пишутся ПРЯМО В УРОВЕНЬ —
* СВЕРХУ: провалившийся потолок = колодец). Минимальный per-room override: * pop_level_set_tile, как в оригинале (curr_room_tiles[…] = …). Раньше тут
* живёт между переходами, применяется в enter_room поверх загруженных тайлов * жила таблица переопределений на 8 записей, и девятая правка молча
* (и для ряда 2 верхнего соседа). (Полноценное per-room modifier-состояние * терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
* в docs/gates_spikes_plan.md, P0.) */ * комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS 0x0E #define TILE_DEBRIS 0x0E
#define TILE_EMPTY 0x00 #define TILE_EMPTY 0x00
#define TILE_FLOOR 0x01 #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 и счётчик «запечь пол» по страницам. /* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
* flash_on — вспышка фона активна (нулевая запись палитры подменена). */ * 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); 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 в рабочие массивы и отрисовать её фон в ОБЕ /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
* активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */ * активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
@@ -199,7 +260,6 @@ static void enter_room(uint8_t room)
pop_guard_leave(); /* запомнить стража уходящей комнаты */ pop_guard_leave(); /* запомнить стража уходящей комнаты */
uint8_t *room_mod; uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); 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(room_fg);
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
pop_room_link(room, 2), pop_room_link(room, 3), 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(); pop_level_access_begin();
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c)); for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
pop_level_access_end(); pop_level_access_end();
apply_overrides(up, above_fg, 20, 10); /* провалившийся потолок = колодец */
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c]; for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
pop_room_set_above(above_fg, above_mod); pop_room_set_above(above_fg, above_mod);
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */ 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(); int8_t dir = (int8_t)~pop_level_start_dir();
uint8_t seq = SEQ_TURN; 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 /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos
* отрабатывает раньше первого draw_room), иначе шов кадр показывал бы * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы
@@ -539,26 +606,26 @@ int main(void)
uint8_t fcol = (uint8_t)(fpos % 10); uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0; pop_loose_fell = 0;
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */ pop_level_set_tile(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) { if (down) {
int8_t lrow = pop_room_col_landing(down, fcol); 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 комнаты сверху) */ if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1); uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
uint8_t up = pop_room_link(cur_room, 2); uint8_t up = pop_room_link(cur_room, 2);
pop_ceil_fell = 0; 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) { /* кусок лёг в ЭТОЙ комнате → щебень */ 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; pop_debris_at = 0;
} }
if (pop_item_taken) { /* предмет поднят → тайл стал полом */ if (pop_item_taken) { /* предмет поднят → тайл стал полом */
item_pos = (uint8_t)(pop_item_taken - 1); item_pos = (uint8_t)(pop_item_taken - 1);
item_bake = 2; /* запечь на ОБЕИХ страницах */ 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_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
pop_item_taken = 0; pop_item_taken = 0;
} }
@@ -588,9 +655,14 @@ int main(void)
} }
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */ PROF(6); /* спрайты: страж, Kid, fore поверх */
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */ { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
PROF(6); /* kid_draw (тот же циан) */ * персонажа (см. guard_over_kid). */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ 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-порядок) */ pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash(); kid_draw_splash();
@@ -598,6 +670,11 @@ int main(void)
} }
pop_hp_draw(); /* полоса HP — только при изменении */ pop_hp_draw(); /* полоса HP — только при изменении */
PROF(6); /* fore поверх Kid + борта */ 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(), pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(), kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action); Kid.direction, Kid.frame, Kid.action);