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:
@@ -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 (вечер)
|
||||
|
||||
<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.
|
||||
|
||||
@@ -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 | ревизия | открыт |
|
||||
|
||||
---
|
||||
|
||||
<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)
|
||||
|
||||
@@ -139,3 +278,26 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
|
||||
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
|
||||
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в
|
||||
настройках; пока просто включены (`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 | закрыт |
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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-слоем. */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -150,6 +160,17 @@ void pop_guard_enter(uint8_t room)
|
||||
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
|
||||
* тайла. */
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
+235
-102
@@ -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 */
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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 /* плита-потолок: дрожащий кадр / покой */
|
||||
|
||||
@@ -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) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
|
||||
|
||||
@@ -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 (порядок как в оригинале) */
|
||||
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
|
||||
* персонажа (см. 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);
|
||||
|
||||
Reference in New Issue
Block a user