From 6673279cef992e6d0eaf2433bd3fd08e83add4e4 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Fri, 7 Aug 2026 22:11:03 +0300 Subject: [PATCH] =?UTF-8?q?PoP:=20=D1=81=D0=BA=D0=B5=D0=BB=D0=B5=D1=82=20?= =?UTF-8?q?=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D1=8F=203,=20=D1=86=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=B6=D0=B5=D0=B9,=20?= =?UTF-8?q?=D0=BE=D0=BA=D0=BA=D0=BB=D1=8E=D0=B7=D0=B8=D1=8F=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=BD=D0=B8=D0=BA=D0=B0;=20=D0=BA=D1=8D?= =?UTF-8?q?=D1=88=20=D1=81=D0=BE=D1=81=D0=B5=D0=B4=D0=BD=D0=B8=D1=85=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BD=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скелет (L3-SKEL, ассеты + механика): - pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3 curr_guard_color = 0, оригинал палитру не подменяет); - pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом стража и был невидим; - load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень — только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался; - check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в комнате 3 при падении (seg002:252), autocontrol_skeleton; - leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня. Цвета стражей (BUG-GUARD-COLOR-1, закрыт): - все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257). Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS, а тот читает только #4000-#BFFF — таблицу из банка копируем в стек. Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт): - pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19. Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать обратно. +48 байт W2. Окклюзия соперника: - pop_fore_over_char получил проход other_overlay_tile (порядок midtable, seg008:1B06) и расширение перебора объединённым прямоугольником «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх кладки и верхней грани пола; - клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP; - ROOMNAV после смерти Кида делает честный pop_start_level: телепорт «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с вернувшейся кучей костей. Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком (101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) — render_room анимированные тайлы пропускает, и в атласе не было ни одного. Число EMM-страниц не изменилось. Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок (резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен гард «код наехал на данные» (DATA_LOC). Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3: level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT). В roomtest.c временно оставлен автостоп на падении соперника (отладка падений скелета) — помечен ВРЕМЕННО. Co-Authored-By: Claude Opus 5 --- applications/PoP/CLAUDE.md | 5 +- applications/PoP/README.md | 2 +- applications/PoP/docs/KID_PLAN.md | 2 +- applications/PoP/docs/PORT_PLAN.md | 10 +- applications/PoP/docs/README.md | 2 +- applications/PoP/docs/gates_spikes_plan.md | 2 +- applications/PoP/docs/layout_plan_v2.md | 6 +- applications/PoP/docs/levels_plan.md | 4 +- applications/PoP/docs/room_model_plan.md | 2 +- applications/PoP/poc/res/bg/pop_env3.atl | Bin 5200 -> 12937 bytes applications/PoP/poc/res/bg/pop_fore.atl | Bin 7763 -> 12359 bytes applications/PoP/poc/res/skel/g0.atl | Bin 0 -> 8267 bytes applications/PoP/poc/res/skel/g1.atl | Bin 0 -> 9893 bytes applications/PoP/poc/res/skel/g2.atl | Bin 0 -> 9347 bytes applications/PoP/poc/res/skel/g3.atl | Bin 0 -> 2633 bytes applications/PoP/roomtest/CLAUDE.md | 14 +- applications/PoP/roomtest/Makefile | 13 +- applications/PoP/roomtest/README.md | 21 +- .../roomtest/{TASKS.md => TASKS_CLOSED.md} | 1165 +++++++---------- applications/PoP/roomtest/TASKS_OPEN.md | 487 +++++++ applications/PoP/roomtest/bug_closed.md | 288 +++- applications/PoP/roomtest/bug_list.md | 278 ++-- applications/PoP/roomtest/bugs_level2.md | 10 + applications/PoP/roomtest/guards.c | 120 +- applications/PoP/roomtest/pop_bg.c | 46 +- applications/PoP/roomtest/pop_bg.h | 8 +- applications/PoP/roomtest/pop_ctrl.c | 3 + applications/PoP/roomtest/pop_gdraw.c | 162 ++- applications/PoP/roomtest/pop_guard.c | 130 +- applications/PoP/roomtest/pop_guard.h | 43 + applications/PoP/roomtest/pop_guard_pal.h | 31 + applications/PoP/roomtest/pop_kid.c | 106 +- applications/PoP/roomtest/pop_map.c | 701 ++++++---- applications/PoP/roomtest/pop_map.h | 40 +- applications/PoP/roomtest/pop_state.c | 6 + applications/PoP/roomtest/pop_state.h | 11 + applications/PoP/roomtest/pop_trob.c | 3 +- applications/PoP/roomtest/pop_tune.h | 2 +- applications/PoP/roomtest/roomtest.c | 78 +- applications/PoP/roomtest/tests-host/Makefile | 13 +- .../PoP/roomtest/tests-host/README.md | 16 + applications/PoP/roomtest/tests-host/scene.c | 52 +- applications/PoP/roomtest/tests-host/scene.h | 14 + applications/PoP/roomtest/tests-host/stubs.c | 83 +- applications/PoP/roomtest/tests-host/stubs.h | 14 + applications/PoP/roomtest/tests-host/t_char.c | 460 +++++++ applications/PoP/toolchain/pop_pack_bg.py | 43 +- applications/PoP/toolchain/pop_pack_guard.py | 118 +- docs/TODO.md | 2 +- docs/libc-reference.md | 2 +- libbgi/common/gfx_set_idle_hook.c | 2 +- libc/include/kbd_raw.h | 2 +- libc/kbd/kbd_raw_poll.c | 2 +- testkit/host-tests.mk | 18 +- tests/kbdpoll/kbdpoll.c | 2 +- 55 files changed, 3340 insertions(+), 1304 deletions(-) create mode 100644 applications/PoP/poc/res/skel/g0.atl create mode 100644 applications/PoP/poc/res/skel/g1.atl create mode 100644 applications/PoP/poc/res/skel/g2.atl create mode 100644 applications/PoP/poc/res/skel/g3.atl rename applications/PoP/roomtest/{TASKS.md => TASKS_CLOSED.md} (52%) create mode 100644 applications/PoP/roomtest/TASKS_OPEN.md create mode 100644 applications/PoP/roomtest/bugs_level2.md create mode 100644 applications/PoP/roomtest/pop_guard_pal.h create mode 100644 applications/PoP/roomtest/tests-host/t_char.c diff --git a/applications/PoP/CLAUDE.md b/applications/PoP/CLAUDE.md index addbf15..2e4bae9 100644 --- a/applications/PoP/CLAUDE.md +++ b/applications/PoP/CLAUDE.md @@ -66,8 +66,9 @@ Princed для DAT v1.0 (контейнер/индекс/чек-сумма, ко статусами). - `roomtest/` — **активная разработка**: уровень 1 целиком (Kid, стражи, ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи — - `roomtest/TASKS.md`, открытые баги — `roomtest/bug_list.md`, закрытые с - разбором корней — `roomtest/bug_closed.md`. + `roomtest/TASKS_OPEN.md` (закрытые с протоколами — + `roomtest/TASKS_CLOSED.md`), открытые баги — `roomtest/bug_list.md`, + закрытые с разбором корней — `roomtest/bug_closed.md`. - `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`. - `bgtest/`, `coltest/` — отдельные проверки фона/коллизии. - `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`). diff --git a/applications/PoP/README.md b/applications/PoP/README.md index a9a8d55..07ba5b8 100644 --- a/applications/PoP/README.md +++ b/applications/PoP/README.md @@ -9,7 +9,7 @@ Kid со всем набором действий, ловушки, ворота, стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука, таймера/HUD, сохранений. -- Что в работе прямо сейчас — [`roomtest/TASKS.md`](roomtest/TASKS.md). +- Что в работе прямо сейчас — [`roomtest/TASKS_OPEN.md`](roomtest/TASKS_OPEN.md). - Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md). - Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md). - Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md). diff --git a/applications/PoP/docs/KID_PLAN.md b/applications/PoP/docs/KID_PLAN.md index 4a2065f..42771ac 100644 --- a/applications/PoP/docs/KID_PLAN.md +++ b/applications/PoP/docs/KID_PLAN.md @@ -13,7 +13,7 @@ > **Документ оставлен как СПРАВОЧНИК по модели персонажа** (`char_type`, > категории `actions_*`, устройство `play_seq`, объём спрайтов) — он нужен > при портировании остальных акторов (скелет, тень, визирь). Текущие -> задачи — `../roomtest/TASKS.md`. +> задачи — `../roomtest/TASKS_OPEN.md`. Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c` (ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей), diff --git a/applications/PoP/docs/PORT_PLAN.md b/applications/PoP/docs/PORT_PLAN.md index 1bbfda7..9633891 100644 --- a/applications/PoP/docs/PORT_PLAN.md +++ b/applications/PoP/docs/PORT_PLAN.md @@ -8,7 +8,7 @@ | Раздел | Что с ним сейчас | |--------|------------------| | §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) | -| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, ``). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS.md` (KBD-1) | +| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, ``). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS_CLOSED.md` (KBD-1) | | §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) | | §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) | | §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой | @@ -18,7 +18,7 @@ | §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` | **Где смотреть текущее состояние, а не план:** `../roomtest/README.md` -(что играется), `../roomtest/TASKS.md` (что в работе), `levels_plan.md` +(что играется), `../roomtest/TASKS_OPEN.md` (что в работе), `levels_plan.md` (следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам). --- @@ -409,7 +409,7 @@ memory/png_strip_padding_tradeoff. осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между комнатами во все четыре стороны. Осталось: **старт по данным уровня** (`pop_level_start_*` реализованы, но не подключены) — задача L1-START в -`../roomtest/TASKS.md`. +`../roomtest/TASKS_OPEN.md`. **Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через `LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой @@ -456,13 +456,13 @@ tiny/small. 1. ~~**Held-state клавиатуры** (§2)~~ — **закрыт** (``). Открытый остаток — не «есть ли held-state», а потеря байт при аккордах - Shift+стрелка: `../roomtest/TASKS.md`, KBD-1. + Shift+стрелка: `../roomtest/TASKS_CLOSED.md`, KBD-1. 2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался: спрайтовый движок для персонажей не используется, поэтому лимит «~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов (~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()` - в `roomtest.c`. План выжимания — `../roomtest/TASKS.md` (CLIP-1) и + в `roomtest.c`. План выжимания — `../roomtest/TASKS_CLOSED.md` (CLIP-1) и `../roomtest/bug_list.md` (T-1/T-2). 3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28 атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5; diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index b973442..c6d7de5 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -6,7 +6,7 @@ | Документ | О чём | |----------|-------| -| [`../roomtest/TASKS.md`](../roomtest/TASKS.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) | +| [`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) | | [`../roomtest/bug_list.md`](../roomtest/bug_list.md) | Открытые баги roomtest (закрытые — в `bug_closed.md` рядом) | | [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | | [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки | diff --git a/applications/PoP/docs/gates_spikes_plan.md b/applications/PoP/docs/gates_spikes_plan.md index fc0377f..8dfc452 100644 --- a/applications/PoP/docs/gates_spikes_plan.md +++ b/applications/PoP/docs/gates_spikes_plan.md @@ -13,7 +13,7 @@ > **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка > объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные > ссылки на механику SDLPoP) продолжают экономить время при отладке. -> Текущие задачи — `../roomtest/TASKS.md`. +> Текущие задачи — `../roomtest/TASKS_OPEN.md`. Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст). Всё сверено с diff --git a/applications/PoP/docs/layout_plan_v2.md b/applications/PoP/docs/layout_plan_v2.md index eeb7f2b..eec9c54 100644 --- a/applications/PoP/docs/layout_plan_v2.md +++ b/applications/PoP/docs/layout_plan_v2.md @@ -460,7 +460,7 @@ dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`, `bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`, `sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`, `libc_one_function_per_module`. -- `applications/PoP/roomtest/TASKS.md` — что из этого берётся в работу сейчас. +- `applications/PoP/roomtest/TASKS_OPEN.md` — что из этого берётся в работу сейчас. --- @@ -487,7 +487,7 @@ memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже **Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`). -**Не закрыт heal** — задача CLIP-1 в `../roomtest/TASKS.md`. +**Не закрыт heal** — задача CLIP-1 в `../roomtest/TASKS_CLOSED.md`. **ВАЖНО:** W3-скобку (`_bgi_begin`/`_bgi_end`) ставит САМА libbgi — вызывать её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято @@ -500,7 +500,7 @@ memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже вместо скобки на блит; нужен публичный batch-API в libbgi. **Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`, длинная серия задержит кадровое прерывание — а по разбору KBD-1 - (`../roomtest/TASKS.md`) длинные DI-окна и есть причина потери байт + (`../roomtest/TASKS_CLOSED.md`) длинные DI-окна и есть причина потери байт клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую. 2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному (до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним diff --git a/applications/PoP/docs/levels_plan.md b/applications/PoP/docs/levels_plan.md index 9b636e5..c913b5f 100644 --- a/applications/PoP/docs/levels_plan.md +++ b/applications/PoP/docs/levels_plan.md @@ -21,7 +21,7 @@ up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`, функции формирования имени. - **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`, `pop_level_start_pos()`, `pop_level_start_dir()` — реализованы и пока - НЕ вызываются (см. `../roomtest/TASKS.md` L1-START). + НЕ вызываются (см. `../roomtest/TASKS_CLOSED.md` L1-START). - **Страж по данным уровня**: `pop_level_guard()` (порт `enter_guard`), сохранение состояния между комнатами (`pop_guard_state_save`). - **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py` @@ -70,7 +70,7 @@ up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`, > **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 → > Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие > колонны рисуются) → уровень 3. Разбор что именно сделано и что по -> уровню 2 осталось — `../roomtest/TASKS.md`, запись **L2**. +> уровню 2 осталось — `../roomtest/TASKS_CLOSED.md`, запись **L2**. > > Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется: > **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина diff --git a/applications/PoP/docs/room_model_plan.md b/applications/PoP/docs/room_model_plan.md index fa2b083..a325e18 100644 --- a/applications/PoP/docs/room_model_plan.md +++ b/applications/PoP/docs/room_model_plan.md @@ -14,7 +14,7 @@ > **Зачем документ остаётся.** Полная straddle-модель понадобится для: > (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4), > (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у -> шва (S4). Брать из `../roomtest/TASKS.md`, когда дойдёт очередь. +> шва (S4). Брать из `../roomtest/TASKS_OPEN.md`, когда дойдёт очередь. Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате, пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP. diff --git a/applications/PoP/poc/res/bg/pop_env3.atl b/applications/PoP/poc/res/bg/pop_env3.atl index 02a97ae48982d694c7a26a2172a3bdb39e4254f4..5a30bf8d1e7a3419e34a0594276db848ec97eb19 100644 GIT binary patch literal 12937 zcmeI2O>P`T6vx}LJyIfBN+KdVaU8~Yfx;?Ck=S^H!~x=Vk3Az9d&`0)$KVRAxds~~ zmRx}aD^9=yHShnv>Q!}hk3;NWMJ~l%Rqyw{>Z`kFGN*69{&L>je))c5zPYi{G{0}m zcToSZ`TP;;z3o?9sK28>)W2-ck5PZKBlT~e%wMAaeu`8$EZzzUELk*??V6A zPxm-7%rHqqm?pR+t2&w@tSP?ze zWht6g7_d~P(BxtdfHblRf=K0kpG^$|OWQs1V0-o>4AX<9k{G6snh1dAlaZ=E+f?@vxPebsQTP-slJtzq3&6%^<%`X>nubA@59 z&WS;#;G{rOa8jsovVuaDL!XSOA`AzGf90&gIg}guZD=crjR(&8zz}JFIcG{4~ej8G6j2t*xKeHnDPsQ z@fgBvQ9D|-!F|uSRN7#bQ>J^Ew&ra-gL}UR@c9{ph#Uv3RN(9=e>wb7p@&E@I1eo- zlsqYr9Vz!v2=?w_X_g6Vd1`g^sV{SwoC^wiF~V1u%is z-c=rQW`(5BiN`Swc*V0CiE6D8e((f$IFy`jQ^xd~pTcNOV4pJCa6v1{ap5C-XzEWh0yulk>sjF*~m{ zf0~c`XE5I*(me1}tC?5q}h@l1DtHGFs< zF?+U5=f~Vj{&>c*=b?hz%U!utkheK~Mk0EqVDEa$@6WJJh}1Xu9{uzd&v3TKc|YjI z&O?;Nww;G4i|ui4o@G}?8)=*6T8@1#ldVp~b2w?XB*vjYyI4GntvqL9WZmUvi(0-C zLOh%#Jg_6>&r0yDyrhvsM{UC`-Op&ES%Abdv3KkYy1*MB>a*VCJG@6_GIS5big%h# zRRr?7?rX(+BJjndSnJwhIQ_az|z z4}tG9hzGGWZ|pv{-|S9(CqMCZJl?VgMc$ez=`jX}BaA+lR~emdd}6OMK~+}$oG*6+ ze1s;CxzB_KjBg^e3^>qkXZpNA@|jmzx<&G~f)xtKGgq>g70KNXNOlvhI){d`kE(D6 zUAjUwSh0Kbqbum3APKIymtQ7Pt{Qt;=m?6e656Bx^tBm5LRvVxY`0mulykP#Oishr zIT9D_0dYt!8^SbwE_8>PJWU*}4LJ!OEMEY)EXS$xVXI1yUoLV z?o!4&bv86hSZB_}p06CHWNyJBr?#0Vu}I3Kc+yDS^t3L`4VQYRZameq=Ah}1xqr+R zUyrHuxZpZ$D0QHH)h7cGxtci`#PJ`?u6^lYN@dnOTbHr=L>3Z1Wh63H5SS+=3A&|0 zw+O^!DYGZ*GD|51$7!Y_hC<#S#(OXk&&F(4JoOkIm91u5EYammpw;*L^m7#OQ(QjU zg3+D!r>FEcHqwa+xSJ@ACygp(8k*&@-fbkKiK8p?=f$iDMRen%8;vvxLK-ic!{Wn2 zfRj!>1f0s{U0GZ;tYcY*lB99_ERXQ+J>Qw(UQ!$7^xZU%o5qjUU10ed=K1{}ISw;; zA4zS751Kc{`IKM%oz(&nI_WP@PR`B-7>Ekd%K=_2$c9RLaz#Hw%Z=3|$*+bKF3BUY z)@(g3iAOf@33?SAb1oWBD1|js&c@v=!U zySAS8!|YhwEiB4X`)uMz@%}xa-5`tFza=c>(sk-zYDTp{E1aqQo_oN>mVUNuPNHK7 nlci_`4neRixOCH19jQG`<1fbD5p!4#Jc}#h(ipi;)2{m$lJWw& delta 109 zcmeB7y`V8+!ek9bg~y>3mh5;Sfv#h85tNj7BUh(m?*?0yXK$3M%_28>sSZeyp;L7XT_47g7KK diff --git a/applications/PoP/poc/res/bg/pop_fore.atl b/applications/PoP/poc/res/bg/pop_fore.atl index 15df455b00d1a29173054eb8e0bf978f1a6d2601..712de619d402a55c690e242869bff67edb2f7926 100644 GIT binary patch literal 12359 zcmeHN&5j#I5T00BB@t*32~o6%6&47I1JW)>#43Rs2M(-f#^XuW&Li*w+<6e5g?Hc$ zIC9~}q4~c0>7Ut|u~!Qc3GT?#{ncMrRaZ^hN$MZI{qm>k-yh#Stf~hOs_MS;_Yaqk z#_U|4EMJb%@4MgorvK-MkFSn-x_ce|cKTv8Z{K|M{MB4Oe>{6OsyD2w-_Ms%=IjjR zKlu1m>~^2%{L5!L|M9ubUw@(VQ17nz=ZnQT{rdG{amM+ti^WsUpI$D+->)tgC&V1f z>QUA18Fk&(bDy@YVPWP2{nVe=|9a0@RxhiNk9M_gn(KAbwoTKlbB?Z^4}o@q z-`3D?ruBu-yiVxL{@8t}EPzvcN%q{@}xQUw$$^^Sc}6Tl`cX z<#*dH;>RIO@WI+5KCs8_t3J0M=jZvK_kY5_EdOKrMSjZ|viyQk@|cT*_?rJ|{eAqs z@n`it7GGBXsE*h9NBp1ZpSCUg@6D}I_U|pW6Eyl@@J5F-->m)ZD3sgWk{{J%A+HBB z@Ai{K!3~M8dknadKf#TB+P|esAL7QN+z@VVL}G$#`jcGY&v3z?NKWiHNq%6oChV*<+Zt)@*A;UPS?1nm-#w`QOVs~|Y9e2dt<1Z6B66y3h zF~-Q5acpMNbUiw5REx;TDlE476UW8fh9^M9O<1L)coS`iv~P-^Tb2=S$uuIQy5{eo zM*LIhNE62J@w|G4yEN@<-q$O=rjs2W5{OJ72Q*|$q0vB2Q=*BABm>uKaGx`6H#(41 zeeRRAP4u57GI?5sNFaVJ%78O6SXMYRCra@6=D}i{BQhl%s;=62Vjr#bRC=0dJyf$u zapno?Y4424%?WjwBrP~8svn7-l4F*n#62Cy3bA*{-4gN)UrXgBVGmp_e81{TSKqC8 zFMv?<@2h<9e#3Amb0Te>9!ZhHmj#9k3=|`|mK`JHVnT71+uGhws;-lt6FcU9sqlnf z*Vlu5F|Wm4rHKYL4$XX-dzxPzp8FJl`;{ns^@57X5>2ehY}NT1mn6Ko}E`KoE!;y;D6K-yNc69q7DaOEonwrNhL z^q{;Vu>`i7RK%xf>8ygMcsKW(eDqy`HaUc31}T|f-{lY}gG31mim>0v*F!VK9g;9T zOHllbP*CJ5atH+bu1j8mA{n9TIaK7rh73AV7!hHg5MYiHPel5SNe&Ppc!`RYBQl6o z#*mO0VNM;QjL#*8Nh{b4PMRkahn}B9IU@(Of>Urvn+XynC@4Zj4hIrltr_x0vFd;c4$fuF(_L3vZ zxbZfD82{FzJ`Bvf;NE+Wb{sRq}LRY z9wE7kXG-7Axve5uz6C4P>qle z2}vda(A3$UbEptJf@21zGcZ?1&XYNopDfTzYEuw9gwXrPdocDAc!%`TqB^g_75~VH r3x(-9&RA4uxprwn&BE|dcTqjfwNx%jCFuqK?kLjD_|>91$*6w=)7h=L delta 848 zcmX?}aM^}6IKa^`b)sPSLVZQ|9n` z%6(3e1L~22-iC+622W7YB&6!;Z6TU{^;AiG3Vh;NL#{(u)E>=36#KSIm{P6I{@%ZE48`umr6ZxLfWk$*Q{?v6Ld?YA7CWGsv$y~^KiGoV$?1cInQiW-qDequ{YIgyIQ z5@`5_w-J$7V;-bd=Z>PJS}sQYL?E`K2&Cw-0B(S(LGYaf^Lk;X2Vz68W0FSF*&q?yd>2n^rui4 zbJQ2)oX`0fJQ&{v={onEZM|R4@o>)>7@u4v#K*f|^P6h3VX<|AxPWx~JBND>bFu<8 zHzx4xWU%v@&}94i)K{u~y^`@FqD0DKO2U==UYHAnB3d=nur?%4fbJ-13R2SYkI=_Da9_-P}t%o;e8>_Bz$&WEks%h^EK(%)Ch`nG6^;* zZ=N!zX*wuZN?s%Zz!a(@d$>KJ%L9)ENzSXEngvSxBKJu@Aa44>|kO& zChf2|i}jc&;ICpk@&N#-J^T)U4%5EgNNo|<>p+xHJ4ZTqB}TO$zsGGiAbJ1`S)t48 zW;Z}ltpYp^DNkKSKt8iBPX%1~!yRNhv8TwR*J#iMsR^J>a2*TCIjM^&0D>=oq7xT( zT$3&o_siG_R<(NKBuP(*WbFcx=ASxoZ+J?rjc&gwd6U{vQ4qf ztT!kKRMDD0Y?0Hb1fU~lFh>z@Omm(uriP@WDIHlVqy(o;n%z%nU7$V4*HIP*=}Z?E zi7Hm3*s?2h=}xT6dXe2bxXX4vzA?M$+^dxswexyB&2_ejl|U=+f6f0Nl+HD9|9-Do zM83s2;mCq8nI$1Iqc2Bs1T**^t`1%aH&x0bka~=W2a^gITq;IH6$K&E-lzoHl0@4! z^&K};2=QT#uVV+0BLxJ<fDAa zLd+#-!kWySsQ9XpRRWx~vfM(2B33?8DciKBb=QRuGd25N(O%=H5L=N8o7rmU<6a&E zG?qH@Y$lJ#bq%;X-Y)*K_8xxs;&S&!o4p&hl)|fUe^kIGn(jFyKS!2s*%u@A0RK_0v_b zTMM}t+r=V3KC;+7gT;RN{QGaOjvs&heLTE>@!~lCdimxZ)<0i;e8l?CFSi@kKYqQv z#rom3?!P_U$bEhM#5g*9INl#m^Xq<}$0Bma6}y39QRQZ2C2@c1A$n3BNsyl^?MZf0 zJ$q!IT?2+PZCcKPD*A^PH?Nbjv=jiK3;{%Lbs=4&;5?ie;I2S5tC)nSsd>1|FMoO^ zf+{JP77br-uyfp0vo`4Gl}2{77WK$b`$HYVoBMgO2UbC zeo9N+f|gHH#bj7-j(2At^^(6Vq0XbvLGBUho{TxbluLNoL{l@S z9;nh)65l{dQb*!06S^7z1B+b_WDwHJno>|nO7LckFPc-r!Gsz+N)0RR-RRJ$SLW9W zGU7D{V#2dsRE^4wNNhz1ww4TUHV`yV7@3e=%{bjb;ix3T*BeE%kA;UVtttXqF&Nj9 zsm>W4^u~Nlg%#%G@p#o|zUcldYkF=A{O9;g!2Q1F?#lxi#qkokKMWVpV7S9TTx2b^ zxLwHOuA^{%drB6?@ZzL5r;f8yj@M!B1Pw*OWNTdHIUIfy3Ri}zT`szC)#t4q?b;ks>9kJE7-UucvKKxoIG(C(J01)?q zcH34XbV14(aV;;_$gm6)8K+o7Xo!S{<&OT4G`PTs8;e{6z=Mv=sPIE)B2?t7Srun% zt!1R~HmToVCDSW+9_ePMp)M~wHU`KM-;THIFS9H?+O0c&4#Hm=KCQtcD=#s|GXlhC zZuHv`mNY-hNHotYYv!Gn%<6eJY$_M%yKdj7u4r{qmTLjdO(OinA=n+$eNOZMMhEdi zbZx6L#CbJwtE#;y(Dunx%O@^aqk=>Wp&3=njOJLT(V56QSIpkQuJ5LUw7t$XDtv|b>vHyv&7%xt72{M^59-oIMbrd0{BtO7uyyjOD z9d}3+@dE+TG*e=MP?cU7t-YD2khUCwFp5q&6s%g#8Lbmj?lqW)$DpTzMo|qF9$Eq{ z1G>A!WwoGan@k}e>J*tFWXOR9l|`V;fG%=3Pm7_Q2pZfn%x14)A!T)KFdGQF+bXqa ziKko$3A0e)7#Av-{R$Bs`7*ogf9vaV&r3|spT-+Zo|eAa)^0HQH6UYYb#^s)b&Erc z$}PHO=c&r12?bPjfM=F2+q#U#5>sT7TgcLtr~6t;NRB!q6ox}mKx;_E*{+mAc1LC> zgQ38Qn;H|w`X-f*DsP7^ zWkf_<$6)`=)pnYyW-(}3U3<(ly#!=NpLYIdRx*BmYvZ|JgY3Lvr;TiJ3)&wp0W-RnhO) z(pN2|g-AeU^mKqQq6*BYC1A67aI({+7Ngr(W>LAL^w5;k!u`=Kr!YRKUd%nxl$-OF f&CSDzY#@8gd714}YZ*S-1%^kwK(>etk^9qk&N3ui literal 0 HcmV?d00001 diff --git a/applications/PoP/poc/res/skel/g2.atl b/applications/PoP/poc/res/skel/g2.atl new file mode 100644 index 0000000000000000000000000000000000000000..b3e1ddb4367d849f9186fa9b6f19351063e1510e GIT binary patch literal 9347 zcmeI0J=vV+XJE2I!B3Kh!0l%+*#-5)uBXNl+ zGJ!K5`|)d!@3~h|e*Wo)@4vkM`}ODb`r*x+>-yu1$9wF*ef9bd`(IzbykP(Jn+M~+ ze5?D9kGJH0|4z^7@b&s|{r$gM9}l|uq|Bz+N3NTu6_97g=sBV|-ZPfJ)amr|(Q9R2 zXKU?A2dVQVlRCFjgE}tiP8#iKZjM5RfTO`nhOD*Ys=gzJhRD!4$S~#r3Npb3d}o?f zvdu%?UZJ>Wr3BJK*v6I!LkJU4c11-PIcq4DUzF2f2e+aEV7Yvn+b25G_dGd!Pnhlw zIlbARREWj4x0}9ltz%g7(e7pThzLRLalL=eZ~d&N&et9FGI+MNK;$|R38Q8KvC&K} z4;SV}#JMH_3%ahYSdBwK|8IV@1zBG-)0zI37>1({ei94EAWc4}0JKbXIphvoax;yqw4dHeQvzdmx z8heQ@VK|<(d0U559fXtP0N4ye>%Uwt&-q(a>+SYp#LfoP6lc~!m04Qx%{8(&+buPF zvpppimrZ%e#Fy4o!sy>>{l@Y89R(9?-+QH8tiJb!=I@bk#hDMRFOK3X2P;{K3MkPR ziS}XW8=dQP5<%c6)H7(r0~*9|De>aiEqIj5M30dWPFUp2u_sW$|5HFqmU76$QVlI! z0BXZeLb0I|GhD_p=<6P-$7FU;9D&SBWn@EOGJ!9QBjWMAf4LhP8XS|0A2A~|c=WRJ zjL2|(bAV}!uT$jM3xTa6)J$qhCkNjz^86$uZo#Ij0Wh#s(R|$EVP#Q1z)mK&Mn&d? zf*K42sU3ERw<+@4m5L3N2@+zdSv^9$m&zKr5VPxPe>aTZybQbWQpe8CDnLCv6B1>w zU5m66n%gllggxT}Ytl0-W-$wzPngf&`7%zM8a{@$@R6i(>W7#di0a~88{K`}WPp7B z&dVq`2#kb7n>xSCZ$qAxMBWN3&R4C^K*@kmf)lx+!$l7L)VRV11S_Y75EsK-4g zuZQV`oc^Fh29-4Eh@DbhnD9zL^Xkdm=3_Vr(o9fS1j0a?4unrUOeqYWBTB=Nx*@@^ z8mK2A>hVf7j*}x!jzoAI4adwl&^h={OItJX>N{pOS_t23VUV(F`R=4d{Y%3pg$gIk z$;6DA$=UA=uDL8Jk&-&OFKqar9C#?Ut07j%tiwN<0H&j_`{C zTV+i`4s>aGi13dyn%gl@VL2340jhd0R)SHo%!k$RNHC33NDpX<@_1lbkK7vq#!>e= ws~H+qv5;XqEglvMdAjcrbR$7aCSCT1am7_T5f|e~#y*%0n=X(m6gwpT0S-dqApigX literal 0 HcmV?d00001 diff --git a/applications/PoP/poc/res/skel/g3.atl b/applications/PoP/poc/res/skel/g3.atl new file mode 100644 index 0000000000000000000000000000000000000000..e13458759b5710da4915ebb307f727b4098515bc GIT binary patch literal 2633 zcmcIj+ie0d5cG*CEx-r-AR0&s=^#?tO1@|-KdFf9%s$Vu@03U}aNeDnon0Gve|vp- zYX84J+VQq5t$iI9S*?8^E+_1t&*v+4tYf>ihp+qHqxrt2xj*hoZ}ZR)#Vp zT>Jp)$;rXk(vw#WDau@8x1*&5Szn6K#6JjAgeY=4a3GDqDd{z7qJyg$pCO?+X_~8q zMi!cY?rVP?GI@>qvc6NVN6xXAwzS8ko$^mKlJ=NgX9xE~I6WC=3{Ol1w(r1Qv5!%O z$8uhBwa}QiI;g-cjT-*}?+*Z5{b*lHZTP%lL{NxUUQlsl9-W!ueid#>loi_8a4XZo zz~Cqe$f#!ooDIDxbQ@{C z9v{0(b`|?g?Ax!d7W1lbEyFI(V(%4NK!th{kVlMznn +### L1-PASS. Сквозное прохождение уровня 1 — **ЗАКРЫТ 2026-08-07** -``` -ур. 1 empty, floor, spike, pillar, gate, closer, potion, loose, debris, - opener, level_door L/R, torch, wall, skeleton, sword ← всё есть -ур. 2 bigpillar_bottom(8), bigpillar_top(9), doortop(12) ← есть (2026-08-04) -ур. 3 chomper(18) ← НЕТ механики -ур. 4 lattice_pillar(25)…lattice_right(29) + тайлсет palace ← отложено -``` +> **Smoke 2026-08-05 (пользователь): успешный.** От старта до выхода с +> уровня одним заходом — меч подобран, **оба стража побеждены в честном бою** +> (без читов), выход отработал корректно. Ценность прогона в том, что он снял +> главные риски этапа 1 разом — боёвка, предметы, переход с уровня — и стал +> регресс-базой для уровня 2. +> +> **Полный обход комнат 2026-08-07 (пользователь): крупных багов нет.** Этим +> же прогоном закрыта [таблица обхода 24 комнат](bug_closed.md#обход-всех-24-комнат-уровня-1) +> и [чек-листы ручной перепроверки фиксов](bug_closed.md#ручная-перепроверка-2026-08-03) +> — обе уехали в `bug_closed.md`. -**Порядок (решение пользователя 2026-08-04): СНАЧАЛА приёмка уровня 2, -потом уровень 3.** Причина техническая, а не вкусовая: чомперы лягут в -тот же банк 2, где живёт отрисовка, и чинить баги фона поверх свежей -механики дороже, чем до неё. +Приёмка этапа 1 и одновременно регресс-база для уровня 2: от старта до двери +уровня одним заходом — подбор меча, страж, кнопки/ворота, пики, loose-полы, +зелье, падения. Точки, где смотрели внимательно, — закрытая косметика +окклюзии (потолок при прыжке вверх, шов при анимации решётки, грани дальней +колонны) и подъём на тайл-кнопку (оговорка к BUG-3 в `bug_closed.md`). -| # | Задача | Что | Блокирует | -|---|--------|-----|-----------| -| 1 | [L2-PASS](#l2-pass) | приёмка уровня 2 + баги отрисовки | ← **сейчас** | -| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 | -| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 | -| 4 | [L3-CHKP](#l3-chkp) | чекпойнт ур. 3 | корректный респавн ур. 3 | -| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | -| — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 | -| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | -| ✔ | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | **СДЕЛАНО 2026-08-05** | -| ✔ | [MEM-BANK5](#mem-bank5) | `pop_ctrl.c` → банк 5 | **СДЕЛАНО 2026-08-05**: куча 180 Б → 2298 Б | + +### L2-PASS. Приёмка уровня 2 — **ЗАКРЫТ 2026-08-07** ---- +> **Smoke 2026-08-05 (пользователь): уровень 2 пройден.** Из smoke пришли +> BUG-LOOSE-3 и BUG-GUARD-DEAF-1 — оба закрыты (`bug_closed.md`). Следом +> прогнан smoke уровня 3. +> +> **Полный обход комнат 2026-08-07 (пользователь): крупных багов нет.** +> Открытыми с этого прогона остались три записи в +> [`bug_list.md`](bug_list.md): [BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1) +> (страж всегда одного цвета), BUG-GUARD-SPLASH-1 (нет брызг при попадании по +> стражу — **закрыт 2026-08-07**, разбор в +> [`bug_closed.md`](bug_closed.md#bug-guard-splash-1)) и +> [BUG-CHEAT-FIGHT-1](bug_list.md#bug-cheat-fight-1) (наш чит `+`/`−` в бою +> отнимает управление). Ни одна играть не мешает. -## Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3 - -Сюда попадает то, что уже работает в проверочном прогоне, но должно быть -подтверждено на сквозных прогонах уровней — потому что задевает механику -шире, чем собственный сценарий. - -- **Зацеп ПРЯМО В ПРЫЖКЕ** (`POP_ENABLE_JUMP_GRAB`, `pop_tune.h`, сделан - 2026-08-06, предварительно проверен пользователем). Почему нужен именно - финальный прогон: точки вызова стоят не только в `check_action`, но и в - ОБЕИХ ветках `check_bumped` — то есть код вклинивается перед обычным - ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о - стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На - уровнях 1–3 это надо специально потрогать в паре мест каждого уровня. - Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — `enable_jump_grab`), - так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при - включённых enhancements. - -## P0 — делаем сейчас - -### L2-PASS. Приёмка уровня 2 — **SMOKE ПРОЙДЕН 2026-08-05** - -> **Прогон 2026-08-05 (пользователь): уровень 2 пройден.** Полная приёмка по -> всем комнатам — позже; из smoke пришли [BUG-LOOSE-3](bug_list.md#bug-loose-3) -> и [BUG-GUARD-DEAF-1](bug_list.md#bug-guard-deaf-1). Следом прогнан smoke -> уровня 3 — его наблюдения в [разделе «Уровень 3»](bug_list.md#уровень-3). - -Баги отрисовки уровня 2 пользователь подаёт списком отдельно — они идут в -[`bug_list.md`](bug_list.md), раздел «Уровень 2». Ниже — **карта -содержимого уровня, снятая прямо с `res2002.bin`**, чтобы приёмка шла по -списку, а не «на глаз»: если механика в таблице есть, а в игре не -сработала — это баг, а не «так задумано». +Ниже — **карта содержимого уровня, снятая прямо с `res2002.bin`**. Она +осталась в архиве не как отчёт, а как справочник для повторных прогонов и +для сравнения с SDLPoP: если механика в таблице есть, а в игре не сработала — +это баг, а не «так задумано». **Стражи — 5, в комнатах 4, 7, 11, 15, 24** (skill 1/2/1/1/3, цвета -1/3/1/1/6 — цвет мы пока игнорируем, атлас один). Заметить: страж +1/3/1/1/6 — цвет мы пока игнорируем, атлас один, см. +[BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1)). Заметить: страж комнаты 24 со skill 3 — первый по-настоящему опасный. **Кнопки и что они открывают** (декодировано из LINKLOC/LINKMAP): @@ -118,572 +91,35 @@ к.24 + СТРАЖ (skill 3) ``` -**Отдельно проверить то, что сделано именно сегодня и на уровне 1 не -проверялось:** +Отдельно проверялось то, что появилось именно на этом уровне: - **большая склянка** (к.20) — потолок HP становится 4, индикатор рисует четыре деления, и это HP **переносится на уровень 3**; -- **меч уже в руках** с самого старта (`have_sword = level >= 2`) — на - уровне 1 его надо было подбирать; -- **выход через дверь к.23** вживую (кнопка в к.9) — путь тот же, что - закрыт на уровне 1 (L1-EXIT), но на этом уровне не прогонялся; +- **меч уже в руках** с самого старта (`have_sword = level >= 2`); +- **выход через дверь к.23** вживую (кнопка в к.9); - **смерть/респавн** возвращают на уровень 2, а не на 1. -### Связность комнат уровней 1–3 (снято 2026-08-05) - -Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет -соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись -13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый, -в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии. - -| уровень | старт | недостижимы | признак | -|---------|-------|-------------|---------| -| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет | -| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы | -| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** | - -``` -ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7 - 23 R→22, у 22 L=4 | 24 R→7, у 7 L=2 - | 24 U→16, у 16 D=0 - на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty -``` - -То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в -брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот -же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю -колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов -там низкий, в игре их не видно. - -**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная -связь — вверх, откуда Кид падает): - -``` -к.10 U→4 пики(2,2) + пол — падение из комнаты 4 -к.14 U→21 шахта 2 тайла шириной, дно = обломки -к.17 U→15 то же -``` - -Это не баги данных: в 14/17 попадают только падением насмерть, а из 10 -(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать -«застрял» багом. - -### L3-CHOMP. Чомперы — механика уровня 3 - -**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти -на каждом уровне, так что это вложение не только в ур. 3. - -**Что портировать:** `animate_chomper` (seg007) — состояние в `room_modif`, -как у пик/ворот, значит шаблон уже отработан (`gates_spikes_plan.md`); -коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка -кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`). - -**Риск:** банк 2 (`pop_bg`) занят на **86.5 %, свободно 2213 Б** — считать -место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся -в потолок» (коммит 2f3e854). Свободные номера банков есть (5+). - -### L3-SKEL. Скелет — единственный противник уровня 3 - -**Важно:** в данных уровня 3 **стражей нет вообще** (`guards_tile` пуст во -всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных», -а **спецсобытие** `check_skel` (seg002:1044): - -> в комнате 1, когда `Kid.curr_col` == 2 или 3 и дверь уровня открыта, -> тайл `tiles_21_skeleton` (комната 1, тайлпос 15) стирается в пол, а на -> его месте поднимается персонаж: `charid_4_skeleton`, меч сразу вынут, -> `seq_88_skel_wake_up`, skill 2, HP 3. - -Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают. - -**Что нужно:** -- `charid_4_skeleton` в `enter_guard`/`pop_guard_enter` (seg002:196): при - `tbl_guard_type[level] == 2` персонаж поднимается **с вынутым мечом** и - последовательностью `seq_63_guard_active_after_fall`, а не - `seq_77_guard_stand_inactive`; -- скелет **бессмертен** — чит `K` его уже не берёт (`pop_guard_kill` - проверяет `CHARID_4_SKELETON`), но и боёвка должна возвращать его к - жизни (seg002:252); -- **новый атлас**: `../SDLPoP/data/SKEL/` (29 файлов) — `pop_pack_guard.py` - сейчас прибит к `GUARD/`, нужен параметр набора (`tbl_guard_dat` = - GUARD/FAT/SKEL/VIZIER/SHADOW). - -### L3-CHKP. Чекпойнт уровня 3 - -`level3_set_chkp` (seg002:519): вход в комнату 7 ставит `checkpoint = 1` и -`hitp_beg_lev = hitp_max`. `do_startpos` (seg003:141) при `checkpoint` -подменяет старт: комната 2, тайлпос 6, направление влево, и убирает -loose-плиту (комната 7, колонка 4, ряд 0). Механика `hitp_beg_lev` уже -есть (сделана в L2), остаётся сам флаг и подмена старта. - - -### TUNE-1. Параметры движка — в конфиг, а не в код - -**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном -заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки -(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`). -Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а -не константой по месту. - -**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без -пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под -моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа, -комментарии `;`, неизвестные ключи игнорировать), файл необязателен — -нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`, -`[debug]`, `[enhancements]`. - -**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) + -`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его -(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и -опций — часть задачи: у него уже разложены по группам стартовые -HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало, -чекпойнт), тайминги ворот и пик, скорости, а отдельной группой — -`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже -портировали). Брать всё подряд не надо: переносим по мере того, как -константа реально понадобилась в игре. - -**Оговорка по памяти.** Парсер и таблица параметров — холодный код, -исполняется один раз при старте: кандидат в банк, а не в резидент W1. - -### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН** - -> **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как -> установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой -> замер потока байт это не подтвердил: клавиатура MAME-Sprinter -> (`pc_kbd ms_naturl`) обёртку `E0 F0 12` / `E0 12` не шлёт вовсе — при -> зажатом Shift поток на стрелку ровно `E0 75 E0 75 …`. Значит удвоения -> трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO -> переполняется» отпадает; сам ФИКС (плотный опрос `kbd_raw_poll`) остаётся -> верным и нужным — переполнение вызывает не Shift, а короткая жизнь -> импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия -> — [BUG-KBD-5](bug_closed.md#bug-kbd-5). -> -> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: при -> зажатом Shift PS/2 удваивает трафик («fake shift»), а импульс запроса -> прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO -> SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен -> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) — -> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча. -> -> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home → -> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже: -> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147. -> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в -> простое). Ниже — полный протокол, как к этому пришли. -> -> **ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно -> лучше, но иногда при зажатом Shift стрелка всё-таки пропускается».** -> Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали, -> значит остаточная частота заметно ниже прежних ~15 %. **Задача осознанно -> ОТЛОЖЕНА до финальной полировки всей программы** (решение пользователя); -> сейчас клавиатура пригодна для работы. -> -> **Где именно осталась дыра — чтобы на полировке не начинать с нуля.** -> Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это -> занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при -> допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё -> ещё может потерять байт — ровно «иногда». Порядок действий, если -> вернёмся: -> 1. Вернуть вызовы `kbd_raw_poll()` между блитами занятой фазы (они -> бесплатны; сами по себе не помогали, но вместе с хуком закрывают -> именно этот зазор) и при необходимости внутрь тайловых циклов -> `pop_bg` — тогда слепым остаётся только тело одного блита. -> 2. Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые -> нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий. -> 3. Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code -> Set 3 через BIOS `$EA` (убирает «fake shift» в корне, но в MAME -> непроверяемо — обратный путь к клавиатуре не разведён) и общий -> `m_irq_off_timer` в драйвере MAME. - -**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при -УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше -нажатия ← не отрабатываются, пока Shift не отпустишь. - -**Рабочая гипотеза (механизм, а не догадка «что-то с клавиатурой»).** -Три известных факта складываются в одну картину: - -1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ - клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием - шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было - бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3). То есть - ровно в связке Shift+стрелка трафик удваивается. -2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то, - что мы успеваем вычерпывать её по ходу. Потерянный make стрелки = - «нажатие не отработало»; потерянный break = залипание (его лечит - `kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш). -3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.** - `mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()` - выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая - запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же - заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию. - **Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт - остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц). -4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит - (`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»); - порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это - сотни микросекунд, на порядки больше 32-тактового импульса. - -Отсюда: **обе версии пользователя — про одно и то же.** Логика ввода -(`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с SDLPoP и -выглядит корректной: `safe_step()` ставит `control_forward = CONTROL_IGNORE`, -и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть -повторные тапы ← при зажатом Shift обязаны работать. Не работают они -потому, что до нас не доезжает либо make, либо break стрелки. - -**План проверки — по шагам, каждый даёт артефакт:** - -1. Счётчики в MAME: брейк на `_kbdraw_overrun` (запись) и на ветке - `tr_kbd_drain` — сколько overrun'ов за 10 с при «Shift зажат, тапаю ←» - против «тапаю ← без Shift». Ожидание по гипотезе: с Shift кратно больше. -2. Замер максимального DI-окна кадра: брейкпоинты на `di`/`ei` в - `_bgi_blit_cols_raw` + `{printf totalcycles; g}` — получить реальную длину - в тактах и в микросекундах. -3. **Спайк «блит без DI».** `docs/new/06-accel.md §6.6`: новая прошивка - допускает работу акселератора при EI (по приходу прерывания он - отключается, по `RETI` включается обратно); старая — нет. Собрать libbgi - с убранным `di` в блит/heal-ядрах, прогнать roomtest в MAME: (а) не - рушится ли картинка, (б) падает ли счётчик overrun из п.1. Если да — - причина подтверждена, и дальше это вопрос «какая прошивка на живом - железе» (по умолчанию оставить DI, режим без DI — опцией libbgi). -4. **Независимо от п.3 — `kbd_raw_poll()`.** Вычерпывание FIFO ОПРОСОМ - (порт `0x19` бит 0 → читать `0x18`, тот же декодер make/break, что в - трамплине) из главного цикла 2–4 раза за кадр между фазами `PROF()`. - Снимает зависимость от «поймали ли мы импульс IRQ» вообще, стоит сотни - тактов, графику не трогает. Реализация: вынести drain-цикл из - `libc/irq/_irq_tramp.c` в общий кусок либо продублировать в - `libc/kbd/kbd_raw_poll.c`; тело обязано идти под `DI` (гонка с ISR за - деструктивное чтение порта 0x18). -5. Побочно сюда же играет **[T-2](bug_list.md#t-2) (idle-skip)**: не - перерисовывать Кида, пока поза/координаты не менялись, — это минус - heal+blit (то есть минус DI-окна) в самых спокойных кадрах, где как раз - и тапают Shift+стрелку. -6. Только если после 3–4 симптом жив — копать логику - `control_shift2`/`CONTROL_IGNORE` против `seg005.c:374..390`. - -**Критерий готовности:** при зажатом Shift десять тапов ← дают десять -осторожных шагов (проверка в MAME через `:kbd:ms_naturl:*` напрямую, НЕ -через `press_key` — тот дёргает обе клавиатуры, см. `docs/libc-reference.md` -``). - --- -### KBD-1: ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась +## Уровни и механика -**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не -спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же -`E0`-префикс и тот же «fake shift», что у стрелок), но игрой игнорируется, -поэтому Кид стоит на месте и рельеф комнаты на результат не влияет. -Брейкпоинты с действием `{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки -машины) в трёх точках: вход клавиатурной ветки трамплина, чтение порта 0x18 -внутри drain-цикла, запись make-бита для кода `0x6C`. Скратч-байты — хвост -`ovr_tile[]` (в этом сценарии не используется). + +### L3-CHKP. Чекпойнт уровня 3 — **СДЕЛАНО 2026-08-06** (коммит 0cd6b2d) -**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до -декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил -пользователь. +`level3_set_chkp` (seg002:0665): флаг взводится, когда Кид уходит **ВЛЕВО ИЗ +комнаты 7**; `do_startpos` (seg003:141) по нему подменяет старт на комнату 2, +тайлпос 6, направление влево и снимает loose-плиту (комната 7, колонка 4, +ряд 0). Константы — в `pop_tune.h` (`POP_CHKP_*`), механика `hitp_beg_lev` +была сделана раньше в L2. -| Прогон | make дошло / нажато | overrun | -|--------|---------------------|---------| -| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 | -| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 | -| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 | +**Тонкость, на которой сначала ошиблись:** `level3_set_chkp` вызван из +`leave_room` ДО `goto_other_room`, поэтому `Char.room == 7` — это комната, ИЗ +которой уходят, а не в которую входят. Поймал пользователь прогоном в +SDLPoP: смерть В комнате 7 вернула его в стартовую 9, а плита осталась цела. -**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет -и прерывания разрешены практически всё время, потерь НЕ меньше, а больше. - -**Вывод 2: `kbd_raw_poll()` в текущей расстановке бесполезен** — 9/10 и с -ним, и без. Причина понятна задним числом: шесть вызовов стоят В ТЕХ ЖЕ -точках, где прерывания и так разрешены, то есть добавляют ровно то, что -трамплин сделал бы сам. Вызовы из `roomtest.c` убраны; сама функция в libc -оставлена — она корректна и нужна как заготовка под «плотный опрос» (см. -ниже), но в горячем цикле её держать не за что. - -**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта -0x18: 5 нажатий Shift+Home должны дать ровно 50 байт (нажатие `E0 F0 12` + -`E0 6C`, отпускание `E0 F0 6C` + `E0 12` = по 10 на цикл). Насчитано **49** -— и ровно один make потерян. То есть до процессора байт не доехал вообще, -декодер тут ни при чём. - -**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же -49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина -(1.75 байта за вход). То есть больше сорока процентов импульсов запроса -не были обслужены, и байты копятся в трёхбайтовом FIFO вплотную к его -потолку; одна неудачная пауза — и байт потерян. - -### KBD-1: ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью - -`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме -`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода: -любая работа разредила бы опрос и испортила замер). Это физический -максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики. - -| Прогон | нажатий | make дошло | байт прочитано / ожидалось | -|--------|---------|-----------|-----------------------------| -| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) | -| Shift + Home | **25** | **25** | **250 / 250** | - -**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах -за кадр терялся 1 байт из 50. При такой частоте потерь вероятность -случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не -совпадение. - -**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно -опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за -60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого. - -**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято -работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая -луч. Опрос там стоит ноль и покрывает две трети периода с запасом по -плотности. Остаётся слепым только тело одного accel-блита под DI (до -~650 мкс) — разорвать его нельзя (см. «что НЕ делать»). - -**Почему нужна именно такая частота (вопрос «PS/2 же не даёт больше 30 -нажатий в секунду»).** Частота опроса определяется НЕ темпом нажатий, а -темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Одно нажатие при -зажатом Shift — это 5 байт подряд (`E0 F0 12`, `E0 6C`), отпускание — ещё 5, -и клавиатура выдаёт их со скоростью провода: 11 бит на байт при ~10–16 кГц -= ~0.7–1.1 мс на байт. Воронка — 3 байта. Значит между двумя вычерпываниями -имеют право прийти максимум два байта, то есть вычерпывать надо не реже чем -раз в ~1.5–2 мс (0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду -переполнит FIFO**, если в эти несколько миллисекунд его никто не разгребает. -Замер это подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости. -В норме разгребает прерывание; опрос понадобился только потому, что ~44 % -импульсов здесь теряется. - -**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не -ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md` -§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать: -- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт, - break = 2. Нажатие с шифтом перестаёт превышать FIFO в принципе. -- либо хотя бы отключить typematic (`0xF5`/`0xF7`). - -**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только -направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь -(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда. -Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это -кандидат на «когда дойдём до реального железа», а не на сейчас. - -**Что сделано по этому плану (2026-08-01):** -1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания - луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`. - Полезен не только нам — любой программе даёт «качать» что-то в ожидании - кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова - нужен push/pop, а сам таймаут в итерациях станет длиннее по времени. - **Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда, - где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60. Полезной - работы не отнимается нисколько. - **Ограничитель области, если «постоянный опрос» всё равно не нравится:** - потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift - потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt — - тогда опрос работает исключительно в той ситуации, ради которой заведён. -2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет. -3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило. - Держать в уме, если на реальном железе или на более тяжёлых сценах - (несколько стражей) потери появятся снова — накрыть блиты дешевле, чем - изобретать что-то новое. -4. ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые - нажатия ровнее человеческих, и «залипания до отпускания Shift» они не - воспроизводили с самого начала. - -**Куда смотреть дальше, если плотного опроса не хватит.** -1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на - его машине занимает часы). Для протокола, подозрение осталось: - `sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта - CPU**, и `m_irq_off_timer` — **один на два источника** (`irq_on()` экрана - заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое - прерывание способно обрезать клавиатурный импульс — правдоподобное - объяснение «44 % пропущенных импульсов». -2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе - проблемы может не быть вовсе. Проверять при первом прогоне на живом - Sprinter. - -**Про совпадение кадрового и клавиатурного прерываний** (вопрос -пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором -`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл, -значит экран»; совпадение возможно, но «исключительно редкий случай» -(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин -делает ровно предписанное. Известный побочный эффект: при совпадении мы -обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на -потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает -повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка, -в KBD-1 не входит. - -**Что НЕ делать (проверено, стоило времени):** -- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di`→`nop` в - `_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в - памяти **уронил машину в перезагрузку**. То есть режим «акселератор - работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции - недоступен — вопрос закрыт артефактом, а не рассуждением. -- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1. - ---- - -### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01** - -> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту, -> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах -> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид -> стоит, стража нет): -> -> | путь | тактов на кадр | кадров в выборке | -> |------|----------------|------------------| -> | клипающее ядро (как было) | **26 200** | 149 | -> | noclip (стало) | **15 848** | 239 | -> -> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при -> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина — -> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то -> есть на той же геометрии и в той же сцене. -> -> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17), -> BANK2 13 792 → **13 676** (−116, свободно стало 2708 Б — это тот самый -> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (−263), -> BANK4 без изменений. -> -> **Грабли, стоившие двух пересборок** (вынесено в память -> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static -> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое -> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит -> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в -> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном -> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без -> трамплина, как у `pop_sword_draw`). -> -> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за -> решёткой шва» (straddle — там как раз работает клипающий фолбэк) — -> артефактов и следов нет. - -**Ниже — исходная постановка задачи (что и почему смотрели).** - - -**Зачем сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта -стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против -4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая -оставшаяся оптимизация: не переписывание логики, а выбор ядра. - -**Что уже правильно** (шаблон, который надо распространить): блиты Кида, -стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в -`gfx_blit_cols_part_noclip`, иначе в клипающий вариант -(`pop_kid.c:86,591,676`, `pop_gdraw.c:105`). - -**Что чинили:** - -- ✅ `kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с - клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им - уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего - ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий - `pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`). -- ⛔ `pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`: - **оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный - параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт - `border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки - тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы - не «оптимизировать» повторно. - -**Что обязано остаться с клипом** (зафиксировано комментариями в коде): -- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется - по построению; -- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной) - и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`; -- борта поля (`pop_room_clip_borders`) — см. выше. - -**Что уже было правильно** (шаблон, который и распространили): блиты Кида, -стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в -`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip -БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по -фиксированным координатам; это тоже помечено в коде. - ---- - -## P1 — сразу после P0 (закрываем уровень 1 как ИГРУ, а не стенд) - -### L1-START. Старт по данным уровня — **СДЕЛАНО 2026-08-01** -Старт и оба рестарта (смерть, выпадение из уровня) сведены в один -`pop_start_level()` — порт `start_level` + `do_startpos` + `set_start_pos` -(seg003): комната/тайл/направление берутся из `pop_level_start_*`, направление -инвертируется (`~start_dir`), поза входа — из `tbl_entry_pose`. У уровня 1 -это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что -захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение -на row 1 — как по данным уровня. - -`#define ROOMNAV` оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в -оригинале не было, — как и `S` (выдать меч), `K`, `I`. Все они со временем -съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01). - -### L1-EXIT. Выход с уровня (дверь уровня) — **СДЕЛАНО 2026-08-01** -Портированы: ветка двери уровня из `up_pressed` + `go_up_leveldoor` -(seg005:0482/0574) — в `pop_leveldoor_enter()` (`pop_map.c`, тайлы и -геометрия) и `up_pressed()` (`pop_ctrl.c`, только последовательность); -опкод `0xF1 END_LEVEL` в `play_seq` теперь инкрементит `pop_next_level` -(порт `next_level`), а главный цикл по нему перезапускает уровень — ровно -та точка, куда `levels_plan.md` §2.2 подключит загрузку уровня 2. - -Открытость двери проверяем по `modifier >= 42` (ветка `fix_exit_door`), а не -по ванильному `leveldoor_open`: с ванильным условием можно войти в ещё -ползущую створку. - -**Грабли, которые стоили отдельного разбора:** `go_up_leveldoor` сначала -писал `Char.x`/`Char.direction`, и оба присваивания молча терялись — окно -`Char` вокруг диспетчера возвращает назад ТОЛЬКО `curr_seq` и `sword` -(`pop_savekid_state`). Направление оставалось «вправо», а все `DX` -последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево. -Вывод на будущее: геометрию персонажа в этом порте меняет `pop_map` (пишет в -`Kid`), а не диспетчер. - -✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком -проёма) — недоставало второй половины `clip_char` (`obj_clip_right`) и -обрезки СПРАВА у колоночного блита. В libbgi добавлен -`gfx_blit_cols_part_w(..., maxw)`; разбор — в [`bug_closed.md`](bug_closed.md). - -### L1-TRIAGE. Ревизия багов — **ЧАСТЬ 1 СДЕЛАНА 2026-08-01** - -✅ **Три Critical'а прогнаны в MAME и закрыты** (протокол с числами — в -[`bug_closed.md`](bug_closed.md), раздел «Проверено в MAME 2026-08-01»): -BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2 -(ping-pong при возврате) **не воспроизводятся**, BUG-3 (окклюзия climb-up на -кнопке) закрыт фиксом `tile_code_drawn` от 2026-07-28. Заодно снят неверный -диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное -поведение `goto_other_room` (`SDLPoP/src/seg002.c:390` меняет только `x`). -Список разделён на [`bug_list.md`](bug_list.md) (открытое) и -[`bug_closed.md`](bug_closed.md) (закрытое + разбор корней). - -✅ **Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были -починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile` -(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty` -(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`), -разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile` -(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md). - -⏳ **Осталось:** закрыть таблицу обхода 24 комнат -([`bug_list.md`](bug_list.md#обход-всех-24-комнат-уровня-1)) — заполнена на -5 строк из 24, инструмент (`ROOMNAV`) готов. Делать вместе с L1-PASS: это -единственный оставшийся источник новых багов уровня 1. - -### L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01) -Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на -логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою -`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()` -= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости -эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою. -Условие «делать ПОСЛЕ CLIP-1» **снято** — CLIP-1 закрыт (см. P0). Проверка — -секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз». - -### L1-PASS. Сквозное прохождение уровня 1 — **SMOKE ПРОЙДЕН 2026-08-05** - -> **Прогон 2026-08-05 (пользователь): успешный.** От старта до выхода с -> уровня одним заходом — меч подобран, **оба стража побеждены в честном бою** -> (без читов), выход отработал корректно. Это smoke: пройдены не все -> комнаты, поэтому таблица обхода 24 комнат остаётся открытой. Ценность -> прогона в том, что он снял главные риски этапа 1 разом — боёвка, -> предметы, переход с уровня — и стал регресс-базой для уровня 2. - -От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота, -пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно -регресс-база для уровня 2. - -Совмещать с [таблицей обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1): -известные баги закрыты, значит новые придут только отсюда. Точки, где стоит -смотреть внимательно, — уже закрытая косметика окклюзии (потолок при прыжке -вверх, шов при анимации решётки, грани дальней колонны) и подъём на -тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md). +**Проверено в MAME:** вход в 7 флаг не ставит, уход влево — ставит; респавн в +комнате 2; после обычной смерти (без чекпойнта) плиты восстанавливаются как +раньше. Вживую в игре (не читом) — потрогать на приёмке уровня 3 +([L3-PASS](TASKS_OPEN.md#l3-pass)). ### L2. Переход на уровень 2 и его игра — **МАШИНЕРИЯ СДЕЛАНА 2026-08-04** @@ -725,55 +161,156 @@ BUG-1 (провал на row 1 при переходе через открыты Shift+L → уровень 3 (комната 9). То есть цепочка загрузок работает повторно, а не только один раз. -**Что осталось по уровню 2 (не машинерия, а контент):** -- сквозное прохождение уровня 2 руками — приёмка, как L1-PASS; -- выход через дверь уровня 2 (комната 23) вживую: путь тот же, что уже - закрыт на уровне 1 (L1-EXIT), но не прогнан на этом уровне; -- цвет стража из данных (`guards_color`, на уровне 2 их три разных) — у нас - один атлас; отдельная задача, к переходам отношения не имеет. +Контент уровня 2 (сквозное прохождение, выход через дверь) закрыт отдельно — +[L2-PASS](#l2-pass). Цвет стража из данных остался открытым багом — +[BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1). -### DBG-CHEATS. Отладочные читы SDLPoP — разведка сделана, код не написан +### L1-START. Старт по данным уровня — **СДЕЛАНО 2026-08-01** -Записано 2026-08-03 (по ходу охоты за BUG-GATE-PASS-1, отложено на потом). -Мотив прямой: мост MAME **теряет нажатия при быстрой отправке**, поэтому -подогнать Кида в нужную позу для отладки автоматикой сейчас нельзя — именно -на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1. +Старт и оба рестарта (смерть, выпадение из уровня) сведены в один +`pop_start_level()` — порт `start_level` + `do_startpos` + `set_start_pos` +(seg003): комната/тайл/направление берутся из `pop_level_start_*`, направление +инвертируется (`~start_dir`), поза входа — из `tbl_entry_pose`. У уровня 1 +это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что +захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение +на row 1 — как по данным уровня. -**Берём (дёшево и бьёт в наш класс багов):** +`#define ROOMNAV` оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в +оригинале не было, — как и `S` (выдать меч), `K`, `I`. Все они со временем +съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01). -- **`[` / `]` — сдвинуть Кида на пиксель влево/вправо.** Оригинал — - `../SDLPoP/src/seg000.c:1828`: - ```c - if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x; - else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x; - ``` - У нас: коды PS/2 set 2 `[` = **0x54**, `]` = **0x5B** в `pop_cheat.h` - рядом с `KBD_CHEAT_KILL/IMMO/SWORD`; обработка — в том же блоке читов - `roomtest.c` (~строка 479), **по фронту** (`*_prev`, как у остальных), - иначе одно нажатие уедет на десяток пикселей. Работает по `Kid.x` - напрямую: геометрию персонажа в этом порте меняет не диспетчер (см. - грабли L1-EXIT). - Первый же потребитель — BUG-GATE-PASS-1: там надо снять `Kid.x` и - `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё стоит - на её тайле. -- **Shift+L — следующий уровень.** Уже входит в L2-машинерию - (`../docs/levels_plan.md` §2.4): `++pop_next_level` и всё. +### L1-EXIT. Выход с уровня (дверь уровня) — **СДЕЛАНО 2026-08-01** -**Остальные — оценка, а не обязательство:** +Портированы: ветка двери уровня из `up_pressed` + `go_up_leveldoor` +(seg005:0482/0574) — в `pop_leveldoor_enter()` (`pop_map.c`, тайлы и +геометрия) и `up_pressed()` (`pop_ctrl.c`, только последовательность); +опкод `0xF1 END_LEVEL` в `play_seq` теперь инкрементит `pop_next_level` +(порт `next_level`), а главный цикл по нему перезапускает уровень — ровно +та точка, куда `levels_plan.md` §2.2 подключила загрузку уровня 2. -| Чит | Вердикт | -|-----|---------| -| **T — таймер** | **не сейчас**: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой | -| **F — остаток feather-fall** | **вместе с Shift+W**: ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно | -| **Shift+F9 — quickload с тем же уровнем** | **самое ценное и самое дорогое**: это сериализация `Char` + `room_modif` всех комнат + trob'ов + стражей (`levels_plan.md` §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки. Кандидат сразу после того, как заработают уровни | +Открытость двери проверяем по `modifier >= 42` (ветка `fix_exit_door`), а не +по ванильному `leveldoor_open`: с ванильным условием можно войти в ещё +ползущую створку. + +**Грабли, которые стоили отдельного разбора:** `go_up_leveldoor` сначала +писал `Char.x`/`Char.direction`, и оба присваивания молча терялись — окно +`Char` вокруг диспетчера возвращает назад ТОЛЬКО `curr_seq` и `sword` +(`pop_savekid_state`). Направление оставалось «вправо», а все `DX` +последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево. +Вывод на будущее: геометрию персонажа в этом порте меняет `pop_map` (пишет в +`Kid`), а не диспетчер. + +✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком +проёма) — недоставало второй половины `clip_char` (`obj_clip_right`) и +обрезки СПРАВА у колоночного блита. В libbgi добавлен +`gfx_blit_cols_part_w(..., maxw)`; разбор — в [`bug_closed.md`](bug_closed.md). + +### L1-TRIAGE. Ревизия багов — **ЗАКРЫТА** (часть 1 — 2026-08-01, хвост — 2026-08-07) + +✅ **Три Critical'а прогнаны в MAME и закрыты** (протокол с числами — в +[`bug_closed.md`](bug_closed.md), раздел «Проверено в MAME 2026-08-01»): +BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2 +(ping-pong при возврате) **не воспроизводятся**, BUG-3 (окклюзия climb-up на +кнопке) закрыт фиксом `tile_code_drawn` от 2026-07-28. Заодно снят неверный +диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное +поведение `goto_other_room` (`SDLPoP/src/seg002.c:390` меняет только `x`). +Список разделён на [`bug_list.md`](bug_list.md) (открытое) и +[`bug_closed.md`](bug_closed.md) (закрытое + разбор корней). + +✅ **Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были +починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile` +(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty` +(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`), +разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile` +(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md). + +✅ **Хвост закрыт 2026-08-07:** таблица обхода 24 комнат уровня 1 — прогоном +всех комнат уровней 1 и 2 (крупных багов нет), см. +[`bug_closed.md`](bug_closed.md#обход-всех-24-комнат-уровня-1). --- -### MEM-BANK5. Разгрузка W1/W2 новым банком кода +## Отрисовка и память + + +### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01** + +> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту, +> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах +> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид +> стоит, стража нет): +> +> | путь | тактов на кадр | кадров в выборке | +> |------|----------------|------------------| +> | клипающее ядро (как было) | **26 200** | 149 | +> | noclip (стало) | **15 848** | 239 | +> +> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при +> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина — +> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то +> есть на той же геометрии и в той же сцене. +> +> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17), +> BANK2 13 792 → **13 676** (−116, свободно стало 2708 Б — это тот самый +> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (−263), +> BANK4 без изменений. +> +> **Грабли, стоившие двух пересборок** (вынесено в память +> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static +> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое +> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит +> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в +> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном +> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без +> трамплина, как у `pop_sword_draw`). +> +> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за +> решёткой шва» (straddle — там как раз работает клипающий фолбэк) — +> артефактов и следов нет. + +**Ниже — исходная постановка задачи (что и почему смотрели).** + +**Зачем было сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта +стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против +4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая +оставшаяся оптимизация: не переписывание логики, а выбор ядра. + +**Что чинили:** + +- ✅ `kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с + клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им + уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего + ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий + `pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`). +- ⛔ `pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`: + **оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный + параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт + `border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки + тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы + не «оптимизировать» повторно. + +**Что обязано остаться с клипом** (зафиксировано комментариями в коде): +- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется + по построению; +- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной) + и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`; +- борта поля (`pop_room_clip_borders`) — см. выше. + +**Что уже было правильно** (шаблон, который и распространили): блиты Кида, +стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в +`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip +БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по +фиксированным координатам; это тоже помечено в коде. + + +### MEM-BANK5. Разгрузка W1/W2 новым банком кода — **СДЕЛАНО 2026-08-05** + +Итог: `pop_ctrl.c` уехал в банк 5, куча 180 Б → **2298 Б** (после снижения +`--max-allocs`, см. [BUILD-FAST](#build-fast), — 2751 Б). Вопрос 2026-08-04: «надо делать новый банк?». **Да, и он лечит именно то, что жмёт.** В нашей раскладке (`MEMORY=huge`, small-вариант) CODE и DATA -живут в ОДНОМ 32-КБ пространстве W1+W2 — карта текущей сборки: +живут в ОДНОМ 32-КБ пространстве W1+W2 — карта сборки на тот момент: ``` _CODE 0x4100..0xAA00 26880 Б @@ -795,28 +332,290 @@ _BSS 0xB9B8..0xBADA 290 Б |--------|------:|-------------------|---------| | `pop_kid.c` | 6287 | `load_frame`/`play_seq` — 2×/кадр (Кид + страж) | частично: холодная половина (загрузка страниц спрайтов, `pop_kid_load`) — да; движок кадров — нет | | `roomtest.c` | 3893 | main-loop | нет (точка входа, зовёт всех) | -| `pop_trob.c` | 2526 | `do_trobs` 1×/кадр, но `pop_trob_modif` — горячий аксессор | **кандидат №1**, если вынести аксессор в резидент | +| `pop_trob.c` | 2526 | `do_trobs` 1×/кадр, но `pop_trob_modif` — горячий аксессор | кандидат, если вынести аксессор в резидент | | `pop_level.c`| 2292 | `pop_level_tile` — из `pop_bg` (банк 2) на каждый тайл | **нет**: банк→банк на каждый тайл убьёт отрисовку | -| `pop_ctrl.c` | 2189 | `user_control` 1×/кадр | **кандидат №2** — дёшево и безопасно | +| `pop_ctrl.c` | 2189 | `user_control` 1×/кадр | **взят** — дёшево и безопасно | | `pop_guard.c`| 923 | 1×/кадр | нет смысла | -Порядок действий, когда упрёмся: `pop_ctrl.c` → банк 5 (2.2 КБ, один -banked-вызов за кадр), затем расщепление `pop_kid.c` на горячее ядро и -холодную загрузку. Критерий кандидата — **не размер, а частота вызова и -отсутствие горячих банк→банк переходов**; `pop_level` показывает, что -большой холодный на вид модуль может быть горячим аксессором. +Критерий кандидата — **не размер, а частота вызова и отсутствие горячих +банк→банк переходов**; `pop_level` показывает, что большой холодный на вид +модуль может быть горячим аксессором. Следующий шаг (расщепление +`pop_kid.c`) — в [`TASKS_OPEN.md`](TASKS_OPEN.md#mem-next), брать по факту +нехватки места. -Брать по факту нехватки места, не заранее. + +### BUILD-FAST. Сборка 10 минут → 1:48 — **СДЕЛАНО 2026-08-06** (коммит 0cd6b2d) + +`--max-allocs-per-node` — во сколько вариантов размещения регистров SDCC +упирается на узел. У `sprinter-cc` дефолт 100000 (агрессивно, как fast-сборки +библиотек), и на крупных модулях roomtest это МИНУТЫ на банк. Дефолт SDCC — +3000, для разработки его достаточно: разница в размере — единицы процента. + +Итог: сборка с нуля **1:48 вместо >10 минут**, куча 2751 Б вместо 2298 +(на 3000 резидент иначе не влезает — замер: конец `_HOME` 0xBC69 при стеке с +0xBB00). В Makefile это `ALLOCS ?= 3000`: + +``` +make — быстрая сборка (ALLOCS=3000) +make ALLOCS=100000 — как раньше: минимальный код, для замеров размера + и для «релизного» образа +``` + +**ВАЖНО:** любое сравнение занятости банков имеет смысл только при ОДНОМ и +том же `ALLOCS` — иначе сравниваются не правки, а уровни оптимизации. + + +### DBG-CHEATS. Отладочные читы SDLPoP — **`[`/`]` СДЕЛАНЫ 2026-08-05**, остальное — оценка + +Мотив прямой: мост MAME **теряет нажатия при быстрой отправке**, поэтому +подогнать Кида в нужную позу для отладки автоматикой нельзя — именно на это +упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1. + +**Сделано:** + +- **`[` / `]` — сдвинуть Кида на пиксель влево/вправо.** Оригинал — + `../SDLPoP/src/seg000.c:1828`: + ```c + if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x; + else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x; + ``` + У нас: коды PS/2 set 2 `[` = **0x54**, `]` = **0x5B** в `pop_cheat.h` + рядом с `KBD_CHEAT_KILL/IMMO/SWORD`; обработка — в том же блоке читов + `roomtest.c`, **по фронту** (`*_prev`, как у остальных), иначе одно + нажатие уедет на десяток пикселей. Работает по `Kid.x` напрямую: + геометрию персонажа в этом порте меняет не диспетчер (см. грабли L1-EXIT). +- **Shift+L — следующий уровень** (вошло в L2-машинерию). + +**Остальные — оценка, а не обязательство** (актуальный статус — +[`TASKS_OPEN.md`](TASKS_OPEN.md), «Отложено осознанно»): + +| Чит | Вердикт | +|-----|---------| +| **T — таймер** | **не сейчас**: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой | +| **F — остаток feather-fall** | **вместе с Shift+W**: ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно | +| **Shift+F9 — quickload с тем же уровнем** | **самое ценное и самое дорогое**: это сериализация `Char` + `room_modif` всех комнат + trob'ов + стражей (`levels_plan.md` §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки | --- -## Отложено осознанно (не брать, пока не появится причина) + +## KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН** (остаток отложен) -- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует. -- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6. -- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти - бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается. -- **Отключение мыши на время игры** и **замена PRNG** — - `../docs/ideas_backlog.md` (оба дают доли процента кадра). -- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость - транзиентная; разбор в [`bug_closed.md`](bug_closed.md). +> **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как +> установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой +> замер потока байт это не подтвердил: клавиатура MAME-Sprinter +> (`pc_kbd ms_naturl`) обёртку `E0 F0 12` / `E0 12` не шлёт вовсе — при +> зажатом Shift поток на стрелку ровно `E0 75 E0 75 …`. Значит удвоения +> трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO +> переполняется» отпадает; сам ФИКС (плотный опрос `kbd_raw_poll`) остаётся +> верным и нужным — переполнение вызывает не Shift, а короткая жизнь +> импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия +> — [BUG-KBD-5](bug_closed.md). +> +> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: импульс +> запроса прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый +> FIFO SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен +> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) — +> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча. +> +> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home → +> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже: +> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147. +> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в +> простое). +> +> **ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно +> лучше, но иногда при зажатом Shift стрелка всё-таки пропускается».** +> Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали, +> значит остаточная частота заметно ниже прежних ~15 %. **Задача осознанно +> ОТЛОЖЕНА до финальной полировки всей программы** (решение пользователя); +> сейчас клавиатура пригодна для работы. +> +> **Где именно осталась дыра — чтобы на полировке не начинать с нуля.** +> Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это +> занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при +> допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё +> ещё может потерять байт — ровно «иногда». Порядок действий, если +> вернёмся: +> 1. Вернуть вызовы `kbd_raw_poll()` между блитами занятой фазы (они +> бесплатны; сами по себе не помогали, но вместе с хуком закрывают +> именно этот зазор) и при необходимости внутрь тайловых циклов +> `pop_bg` — тогда слепым остаётся только тело одного блита. +> 2. Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые +> нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий. +> 3. Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code +> Set 3 через BIOS `$EA` (убирает «fake shift» в корне, но в MAME +> непроверяемо — обратный путь к клавиатуре не разведён) и общий +> `m_irq_off_timer` в драйвере MAME. + +**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при +УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше +нажатия ← не отрабатываются, пока Shift не отпустишь. + +**Рабочая гипотеза, с которой начинали.** Три факта складывались в одну +картину: + +1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ + клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием + шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было + бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3). + (**Опровергнуто замером 2026-08-05, см. поправку выше.**) +2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то, + что мы успеваем вычерпывать её по ходу. Потерянный make стрелки = + «нажатие не отработало»; потерянный break = залипание (его лечит + `kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш). +3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.** + `mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()` + выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая + запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же + заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию. + **Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт + остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц). +4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит + (`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»); + порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это + сотни микросекунд, на порядки больше 32-тактового импульса. + +Логика ввода (`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с +SDLPoP и корректна: `safe_step()` ставит `control_forward = CONTROL_IGNORE`, +и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть +повторные тапы ← при зажатом Shift обязаны работать. Не работали они +потому, что до нас не доезжал либо make, либо break стрелки. + +### ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась + +**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не +спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же +`E0`-префикс), но игрой игнорируется, поэтому Кид стоит на месте и рельеф +комнаты на результат не влияет. Брейкпоинты с действием +`{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки машины) в трёх точках: вход +клавиатурной ветки трамплина, чтение порта 0x18 внутри drain-цикла, запись +make-бита для кода `0x6C`. Скратч-байты — хвост `ovr_tile[]`. + +**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до +декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил +пользователь. + +| Прогон | make дошло / нажато | overrun | +|--------|---------------------|---------| +| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 | +| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 | +| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 | + +**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет +и прерывания разрешены практически всё время, потерь НЕ меньше, а больше. + +**Вывод 2: `kbd_raw_poll()` в той расстановке бесполезен** — 9/10 и с ним, и +без. Причина понятна задним числом: шесть вызовов стояли В ТЕХ ЖЕ точках, +где прерывания и так разрешены. Вызовы из `roomtest.c` убраны; сама функция +в libc оставлена — она корректна и нужна как заготовка под «плотный опрос». + +**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта +0x18: 5 нажатий Shift+Home должны дать ровно 50 байт. Насчитано **49** — и +ровно один make потерян. То есть до процессора байт не доехал вообще, +декодер тут ни при чём. + +**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же +49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина +(1.75 байта за вход). Байты копятся в трёхбайтовом FIFO вплотную к потолку; +одна неудачная пауза — и байт потерян. + +### ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью + +`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме +`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода: +любая работа разредила бы опрос и испортила замер). Это физический +максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики. + +| Прогон | нажатий | make дошло | байт прочитано / ожидалось | +|--------|---------|-----------|-----------------------------| +| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) | +| Shift + Home | **25** | **25** | **250 / 250** | + +**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах +за кадр терялся 1 байт из 50. При такой частоте потерь вероятность +случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не +совпадение. + +**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно +опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за +60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого. + +**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято +работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая +луч. Опрос там стоит ноль и покрывает две трети периода с запасом. +Остаётся слепым только тело одного accel-блита под DI (до ~650 мкс) — +разорвать его нельзя (см. «что НЕ делать»). + +**Почему нужна именно такая частота** (вопрос «PS/2 же не даёт больше 30 +нажатий в секунду»). Частота опроса определяется НЕ темпом нажатий, а +темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Клавиатура выдаёт байты +со скоростью провода: 11 бит на байт при ~10–16 кГц = ~0.7–1.1 мс на байт. +Воронка — 3 байта. Значит между двумя вычерпываниями имеют право прийти +максимум два байта, то есть вычерпывать надо не реже чем раз в ~1.5–2 мс +(0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду переполнит FIFO**, +если в эти несколько миллисекунд его никто не разгребает. Замер это +подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости. В норме +разгребает прерывание; опрос понадобился только потому, что ~44 % импульсов +здесь теряется. + +**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не +ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md` +§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать: +- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт, + break = 2; +- либо хотя бы отключить typematic (`0xF5`/`0xF7`). + +**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только +направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь +(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда. +Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это +кандидат на «когда дойдём до реального железа». + +**Что сделано по этому плану (2026-08-01):** +1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания + луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`. + Полезен не только нам — любой программе даёт «качать» что-то в ожидании + кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова + нужен push/pop, а сам таймаут в итерациях станет длиннее по времени. + **Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда, + где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60. + **Ограничитель области, если «постоянный опрос» не нравится:** потери + случаются ТОЛЬКО при зажатом модификаторе (замерено), значит хук можно + взводить лишь пока нажат Shift/Ctrl/Alt. +2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет. +3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило. + Держать в уме, если на реальном железе или на более тяжёлых сценах + (несколько стражей) потери появятся снова — накрыть блиты дешевле, чем + изобретать что-то новое. +4. ⏳ Ручная проверка пользователем — «стало значительно лучше, иногда всё + ещё пропускает»; остаток отложен (см. врезку в начале записи). + +**Куда смотреть дальше, если плотного опроса не хватит.** +1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на + его машине занимает часы). Для протокола, подозрение осталось: + `sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта + CPU**, и `m_irq_off_timer` — **один на два источника** (`irq_on()` экрана + заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое + прерывание способно обрезать клавиатурный импульс — правдоподобное + объяснение «44 % пропущенных импульсов». +2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе + проблемы может не быть вовсе. Проверять при первом прогоне на живом + Sprinter. + +**Про совпадение кадрового и клавиатурного прерываний** (вопрос +пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором +`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл, +значит экран»; совпадение возможно, но «исключительно редкий случай» +(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин +делает ровно предписанное. Известный побочный эффект: при совпадении мы +обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на +потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает +повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка. + +**Что НЕ делать (проверено, стоило времени):** +- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di`→`nop` в + `_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в + памяти **уронил машину в перезагрузку**. То есть режим «акселератор + работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции + недоступен — вопрос закрыт артефактом, а не рассуждением. +- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1. diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md new file mode 100644 index 0000000..4524b18 --- /dev/null +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -0,0 +1,487 @@ +# roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-07) + +Что берём в работу сейчас и в каком порядке. Каждая запись: что сделать, +почему именно сейчас, чем подтверждать результат. + +- закрытые задачи с протоколами и замерами — [`TASKS_CLOSED.md`](TASKS_CLOSED.md); +- открытые баги — [`bug_list.md`](bug_list.md), закрытые с разбором корней — + [`bug_closed.md`](bug_closed.md); +- планы фаз — `../docs/PORT_PLAN.md`, `../docs/layout_plan_v2.md`, + `../docs/levels_plan.md`. + +**Состояние на 2026-08-07:** пользователь прогнал ВСЕ комнаты уровней 1 и 2 — +крупных багов нет. Приёмки [L1-PASS](TASKS_CLOSED.md#l1-pass) и +[L2-PASS](TASKS_CLOSED.md#l2-pass) закрыты; с прогона открыты три записи по +уровню 2 (цвет стража, брызги по стражу, чит `+`/`−` в бою) — все в +[`bug_list.md`](bug_list.md). Уровень 3 играется, но приёмки не было: сначала +чомперы и скелет. + +Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга; +диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не +гипотезой (memory `defer_unexplained_quirks`). + +--- + +## ТЕКУЩАЯ ЦЕЛЬ: уровни 1–3 (подземелье) отлажены целиком + +Решение 2026-08-04: **palace (уровни 4+) откладываем**, доводим до +играбельности три dungeon-уровня. Основание — они не требуют ни одного +нового ассета фона: инвентарь тайлов, снятый с `res200N.bin`, показывает, +что новое появляется только так — + +``` +ур. 1 empty, floor, spike, pillar, gate, closer, potion, loose, debris, + opener, level_door L/R, torch, wall, skeleton, sword ← всё есть +ур. 2 bigpillar_bottom(8), bigpillar_top(9), doortop(12) ← есть (2026-08-04) +ур. 3 chomper(18) ← НЕТ механики +ур. 4 lattice_pillar(25)…lattice_right(29) + тайлсет palace ← отложено +``` + +| # | Задача | Что | Блокирует | +|---|--------|-----|-----------| +| 1 | [GUARD-PHYS](#guard-phys) | физика стража = физика Кида (одна над `Char`) | **ядро сделано 2026-08-07**; открыт живой сценарий в MAME | +| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 | +| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 | +| 4 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели | +| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | +| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | + +Сделанное по этой цели — в [`TASKS_CLOSED.md`](TASKS_CLOSED.md): L2 (машинерия +уровней), L2-PASS, L1-PASS, [L3-CHKP](TASKS_CLOSED.md#l3-chkp) (чекпойнт +уровня 3), MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS. + +--- + +## Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3 + +Сюда попадает то, что уже работает в проверочном прогоне, но должно быть +подтверждено на сквозных прогонах уровней — потому что задевает механику +шире, чем собственный сценарий. + +- **Зацеп ПРЯМО В ПРЫЖКЕ** (`POP_ENABLE_JUMP_GRAB`, `pop_tune.h`, сделан + 2026-08-06, предварительно проверен пользователем). Почему нужен именно + финальный прогон: точки вызова стоят не только в `check_action`, но и в + ОБЕИХ ветках `check_bumped` — то есть код вклинивается перед обычным + ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о + стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На + уровнях 1–3 это надо специально потрогать в паре мест каждого уровня. + Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — `enable_jump_grab`), + так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при + включённых enhancements. + Прогон 2026-08-07 (уровни 1 и 2) регрессий рядом не показал, но специально + на удар о стену с Shift не проверялся. + +## P0 — делаем сейчас + +### GUARD-PHYS. Страж живёт по тем же правилам, что Кид — **ЯДРО СДЕЛАНО 2026-08-07** + +> **Что уже работает** (решение пользователя: переносим физику на `Char`, +> без предварительных замеров — иначе третий-четвёртый экземпляр той же +> логики неизбежен). +> +> - **физика переведена на `Char`**: `pop_map.c` целиком работает с активным +> персонажем, а кто в `Char` — решают окна `loadkid`/`savekid` и +> `loadshad`/`saveshad`, как в оригинале (seg006:809). Два входа: +> `pop_phys_tick` (порт хвоста play_kid_frame) и `pop_guard_phys_tick` +> (порт play_guard_frame) — списки вызовов отличаются ровно тем, чем в +> оригинале; +> - **`take_hp` стал общим** (`pop_take_hp` в резиденте `pop_guard.c`): урон +> идёт тому, кто в `Char`, по `charid`. Раньше у боёвки и у физики были +> свои копии, причём у физики неверная — правила `hitp_curr` мимо дельты и +> про не-Кидов не знала; +> - **порт веток по charid в `land()`** (seg005:173): страж гибнет с двух +> рядов, тень падает как с одного, у не-Кида приземление даёт боевую +> стойку; `check_guard_bumped` (seg004:0522), `droppedout` + +> `guard_follows_kid_down` (seg002:09F8), `check_guard_fallout` +> (seg002:0241); +> - **`pop_savekid_state` снова полное `Kid = Char`**, а +> `pop_load_fram_det_col` пересчитывает колонку ЛЮБОМУ персонажу — обе +> заплатки существовали только потому, что физика знала один `Kid`. +> +> **Проверено:** `tests-host` — трассы Кида не изменились (`[phys] ok: 1723`, +> тот же эталон), новый набор `t_char` (32 проверки) покрывает ветки по +> персонажам; в MAME проверено, что игра жива (респавн, бег, падение, +> приземление). Цена: `_CODE` +80 Б, банк 3 (`pop_map`) 7973 → 8485 +> (51.8 %), банк 1 (`guards`) 2042 → 2156. +> +> **Проверено пользователем 2026-08-07:** страж СПРЫГИВАЕТ ЗА КИДОМ на ряд +> ниже — связка «ИИ + физика» работает вживую, не только в тестах. +> +> **`follow_guard` портирован и проверен в MAME (2026-08-07):** уровень 1, +> бой в комнате 3, Кид отступает влево — страж приходит следом (`Guard.room` +> 3 → 2, X перенесён через шов), ровно как в SDLPoP. Условия отбора +> покрыты тестами `t_char` (7 сценариев: пороги 91/165, «не бой», мёртвый, +> вверх/вниз, занятая соседняя комната). **Сцена вскрыла отдельный баг — +> [BUG-SWORD-GHOST-1](bug_list.md#bug-sword-ghost-1): при переходе в бою Кид +> прячет меч и дальше дерётся пустой рукой.** +> +> **Осталось (потому и запись открыта):** +> 1. `check_chomped_guard` — вместе с [L3-CHOMP](#l3-chomp); +> 2. ветки `check_guard_fallout` для тени и скелета (скелет возрождается в +> комнате 3) — вместе с [L3-SKEL](#l3-skel); +> 3. страж, нажимающий напольную кнопку, вживую не проверялся (код — +> общий `check_press`). + +**Почему это была ОДНА физика, а не «сделаем стражу свою».** В оригинале +слот `Guard` — не «стражи», а все НЕ-Киды: тем же `play_guard_frame` ходят +**страж** (charid 2), **скелет** (4, ур. 3), **тень** (1, ур. 4/5/6/12), +**визирь-Джаффар** (ур. 13), **толстяк** (FAT, ур. 12) и **мышь** (0x18, +ур. 8) — `tbl_guard_type` = {0,0,0,2,0,0,1,0,0,0,0,0,4,3,−1,−1}, а +`autocontrol_opponent` (seg002:628) разводит их ТОЛЬКО по ИИ. То есть +второй экземпляр логики пришлось бы делать не один раз, а пять. + +**Гард по X: страж не уходит САМ — но его МОГУТ ПЕРЕНЕСТИ.** Поправка к +формулировке, которая была здесь раньше («страж не покидает комнату ни в +каком виде») — она неверна, контрпример дал пользователь: в SDLPoP страж из +комнаты 3 оказывается в комнате 2 вслед за отступающим Кидом. + +Разделять надо два разных механизма: + +- **своим ходом — не может.** Физика персонажа слота Guard обёрнута + `Char.room == drawn_room` и `Char.x >= 44 && Char.x < 211` (seg000:1252), + и никакого `check_leave` в его списке вызовов нет. Провалившегося ниже + комнаты убирает `check_guard_fallout` (seg002:0241) — вниз он не уходит. +- **следом за Кидом — переносит движок.** `exit_room` (seg002:03C7) + вызывается ПОСЛЕ того, как комнату сменил Кид, и решает судьбу стража: + + ```c + if (Guard.alive < 0 && Guard.sword == sword_2_drawn) { // жив и В БОЮ + if (guards_tile[kid_room−1] >= 30 || // в новой комнате + guards_seq_hi[kid_room−1] != 0) { // своего живого нет + if (ушёл ВЛЕВО) { if (Guard.x >= 91) leave = 1; } // страж далеко — остаётся + else if (ВПРАВО) { if (Guard.x < 165) leave = 1; } + else if (ВВЕРХ) { if (Guard.curr_row >= 0) leave = 1; } // всегда → не идёт + else { if (Guard.curr_row < 3) leave = 1; } // вниз → не идёт + } else leave = 1; + } else leave = 1; + leave ? leave_guard() : follow_guard(); + ``` + + `follow_guard` (seg002:039E) стирает `guards_tile` в ОБЕИХ комнатах + (0xFF — «стража здесь нет», чтобы он не раздвоился) и гонит стража через + `goto_other_room` в окне `loadshad`/`saveshad`. + +**Что это значит для нас.** У нас в `enter_room` (`roomtest.c:265`) стоит +безусловный `pop_guard_leave()` — то есть всегда ветка `leave_guard`, и +страж ВСЕГДА остаётся. Портировать надо сам `exit_room`-выбор: условия +«жив + меч вынут + в целевой комнате нет своего стража + он у нужного +края» и `follow_guard`. Пороги 91/165 — это «страж у того края, в который +ушёл Кид»; вверх и вниз оригинал не пускает никогда. + +**Чем подтверждать:** уровень 1, комната 3 — начать бой, отступить влево в +комнату 2: страж обязан прийти следом и продолжить бой (как в SDLPoP на +скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ +живой страж — переход не происходит. + +### L3-CHOMP. Чомперы — механика уровня 3 + +**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти +на каждом уровне, так что это вложение не только в ур. 3. + +**Что портировать:** `animate_chomper` (seg007) — состояние в `room_modif`, +как у пик/ворот, значит шаблон уже отработан (`gates_spikes_plan.md`); +коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка +кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`). + +**Риск:** банк 2 (`pop_bg`) занят на **90.8 %, свободно 1503 Б** (замер +2026-08-07, было 2213 Б) — считать +место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся +в потолок» (коммит 2f3e854). Свободные номера банков есть (5+). + +**Ассеты УЖЕ упакованы (2026-08-07):** `pop_pack_bg.py` кладёт в атлас весь +набор кадров чомпера явно (`CHOMPER_BOT_IDS` 101-105, `CHOMPER_TOP_IDS` +111-113, `CHOMPER_FORE_IDS` 106-110 + кровь 114-123 mono-силуэтом цветом 12). +Раньше в атласе не было НИ ОДНОГО его кадра: `render_room.py` пропускает +анимированные тайлы, а `tile_table[0x12].base_id = 0` — отсюда и «чомпера не +видно вовсе». Рост: `pop_env3.atl` 5200 -> 12937 Б, `pop_fore.atl` 7763 -> +12359 Б, число EMM-страниц НЕ изменилось (7). + +**Чем подтверждать:** комната 22 уровня 3 — чомпер (2,6) анимируется и +рисуется (сейчас его не видно вовсе, см. раздел «Уровень 3» в +[`bug_list.md`](bug_list.md)); смерть Кида в сомкнутых челюстях. + +### L3-SKEL. Скелет — единственный противник уровня 3 + +**Важно:** в данных уровня 3 **стражей нет вообще** (`guards_tile` пуст во +всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных», +а **спецсобытие** `check_skel` (seg002:1044): + +> в комнате 1, когда `Kid.curr_col` == 2 или 3 и дверь уровня открыта, +> тайл `tiles_21_skeleton` (комната 1, тайлпос 15) стирается в пол, а на +> его месте поднимается персонаж: `charid_4_skeleton`, меч сразу вынут, +> `seq_88_skel_wake_up`, skill 2, HP 3. + +Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают. + +> **Сделано 2026-08-07 (ждёт живой проверки на уровне 3):** +> - **атлас скелета** — `pop_pack_guard.py SKEL` собирает `poc/res/skel/g0..g3.atl` +> (28 кадров, 30 КБ, 4 EMM-страницы); упаковщик получил параметр набора +> (`GUARD`/`SKEL`). Палитра у скелета СВОЯ (`SKEL/res750.pal`), а не из +> `res10.bin`: на уровне 3 `tbl_guard_type != 0`, значит +> `curr_guard_color = 0` и оригинал палитру не подменяет вовсе; +> - `pop_guard_load` выбирает набор по типу уровня и заливает палитру +> скелета; Makefile кладёт атласы в `SKEL\` на диск; +> - **`check_skel`** (seg002:1042) — порт в `guards.c`: уровень 3, комната 1, +> дверь уровня открыта, `Kid.curr_col` 2 или 3, тайл 21 на (5,1) → тайл +> стирается в пол (обе страницы), персонаж встаёт с `seq_88_skel_wake_up`, +> мечом наголо, skill 2, HP 3. Зовётся из главного цикла ДО логики +> персонажей, как в `play_frame`; +> - **`leveldoor_open`** — флаг появился (`pop_state.c`), взводит анимация +> двери при `modif >= 43` (seg007:456); +> - **`enter_guard`** — ветка `charid_4_skeleton`: встаёт сразу активным +> (меч вынут), а не в стойке покоя; +> - **возрождение** — `check_guard_fallout`: упавший скелет, под комнатой +> которого лежит комната 3, появляется там снова (x 133, ряд 1); +> - **ИИ** — `autocontrol_skeleton` (seg002:685): меч у скелета вынут всегда. +> +> Регресс: `tests-host` зелёные, `t_char` вырос до 65 проверок (добавлены два +> сценария возрождения). Цена: `_CODE` +80 Б, банк 1 (`guards`) 2342 → 2519, +> банк 4 (`gdraw`) 3875 → 4269. +> +> **Не проверено вживую:** сцена требует пройти уровень 3 до кнопки, которая +> открывает выход — без открытой двери скелет по условию не встаёт. + +**Что нужно:** +- `charid_4_skeleton` в `enter_guard`/`pop_guard_enter` (seg002:196): при + `tbl_guard_type[level] == 2` персонаж поднимается **с вынутым мечом** и + последовательностью `seq_63_guard_active_after_fall`, а не + `seq_77_guard_stand_inactive`; +- скелет **бессмертен** — чит `K` его уже не берёт (`pop_guard_kill` + проверяет `CHARID_4_SKELETON`), но и боёвка должна возвращать его к + жизни (seg002:252); +- **новый атлас**: `../SDLPoP/data/SKEL/` (29 файлов) — `pop_pack_guard.py` + сейчас прибит к `GUARD/`, нужен параметр набора (`tbl_guard_dat` = + GUARD/FAT/SKEL/VIZIER/SHADOW). Здесь же удобно закрыть + [BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1): палитра стража + подменяется по `guards_color`, и оба изменения живут в одном упаковщике. + +### L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный) + +**Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В +оригинальной VGA-версии кладка уровня 3 ЗЕЛЁНАЯ, а уровней 1-2 — серо-синяя. +В SDLPoP все подземелья одинаковые, поэтому по нему разницу не увидеть. + +**Почему в SDLPoP её нет — проверено, не гипотеза.** Механизм там ЕСТЬ +(seg000:1140, «Level colors (1.3)»): + +```c +int level_color = custom->tbl_level_color[current_level]; +if (level_color != 0) { + byte* env_pal = level_var_palettes + 0x30*(level_color-1); + byte* wall_pal = env_pal + 0x30 * custom->tbl_level_type[current_level]; + set_pal_arr(0x50, 0x10, (rgb_type*)env_pal); /* chtab_6 environment */ + set_pal_arr(0x60, 0x10, (rgb_type*)wall_pal); /* chtab_7 wall */ +} +``` + +`tbl_level_color` (data.h:842) = `{0,0,0,1,0,0,0,1,2,2,0,0,3,3,4,0}` — у +уровня 3 цвет **1**, у 7 тоже 1, у 8/9 — 2, у 12/13 — 3, у 14 — 4. Но +`level_var_palettes` — это ресурс **20** из `PRINCE.DAT` (только версии +1.3/1.4), а в `SDLPoP/data/PRINCE/` его НЕТ: там лежит лишь `res10.bin` +(палитры стражей). Значит `level_var_palettes == NULL` и вся ветка молча +пропускается — отсюда одинаковые подземелья. + +**Данные у нас есть.** В `../MSDOS/PRINCE.DAT` ресурс 20 присутствует: +offset 22790, **240 байт** = 5 палитр × 16 цветов × 3 байта (6-битные +каналы, как res10). + +**Что делать (когда дойдём до вида уровня 3).** +1. Достать ресурс 20 из `MSDOS/PRINCE.DAT` (упаковщику придётся читать сам + `.DAT` — сейчас все скрипты берут распакованные PNG из SDLPoP); +2. сгенерировать таблицу палитр рядом с `pop_guard_pal.h`; +3. при загрузке уровня заливать слоты **0x50..0x5F** (env) и **0x60..0x6F** + (wall) — у нас ровно эти базы (`pop_pack_bg.load_indexed`: `pal_base = + 0x60` для WALL, `0x50` для env), то есть совпадение со `set_pal_arr` + один в один; +4. `wall_pal = env_pal + 0x30 * tbl_level_type[level]` — для подземелья + (`level_type == 0`) обе палитры одинаковые. + +**Грабли, уже пойманные на цвете стражей:** `gfx_pal_load` отдаёт указатель +в BIOS (`$A4` через `rst #0x08`), а BIOS читает только `#4000-#BFFF` — +таблицу нельзя передавать прямо из банка (0xC000+), надо копировать в +W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)). + +### DRAW-CHAR. Отрисовка — ОДНА на всех Char, как физика после GUARD-PHYS + +**Почему заведено (пользователь, 2026-08-07).** После GUARD-PHYS физика стала +общей над `Char`, а ОТРИСОВКА так и осталась продублированной: `kid_draw` и +`pop_guard_draw` считают `load_frame_to_obj`, флип и heal-прямоугольники +каждая по-своему, окклюзия — двумя проходами (`pop_fore_over_kid` / +`pop_fore_over_char`). Общее вынесено лишь частично (`char_footprint`, +`other_overlay_tile`, `pop_sword_draw`), а СПИСКИ ВЫЗОВОВ разные. + +**В оригинале она общая — проверено.** `seg008` держит два входа с +ИДЕНТИЧНЫМ телом: + +``` +add_kid_to_objtable (seg008:22F0) add_guard_to_objtable (seg008:2324) + loadkid() loadshad() + load_fram_det_col() load_fram_det_col() + load_frame_to_obj() load_frame_to_obj() + stuck_lower() stuck_lower() + set_char_collision() set_char_collision() + set_objtile_at_char() set_objtile_at_char() + redraw_at_char() redraw_at_char() + redraw_at_char2() redraw_at_char2() + clip_char() clip_char() + add_objtable(0) add_objtable(1 тень / 2 страж) +``` + +Различаются ТОЛЬКО окно (`Kid`/`Guard`), тип объекта и спецкейс тени у +зеркала (уровень 4). То есть та же схема, что `play_kid_frame` / +`play_guard_frame` у физики. + +**Цена дублирования уже видна.** Скелет, падающий в пропасть (ур. 3), +рисовался ПОВЕРХ верхней грани пола соседней колонки: проход +`other_overlay_tile` (порядок midtable — «тайлы позже персонажа ложатся +поверх него») вызывался только у Кида. Симптом закрыт 2026-08-07 +переносом прохода в `pop_fore_over_char`, но корень — именно дублирование. +Раньше это не всплывало потому, что ОБЫЧНЫЙ страж в пропасть не падает: его +физика гейтится `x ∈ [44,211)`, а провалившегося убирает +`check_guard_fallout`. Тем же путём пойдут тень (ур. 4/5/6/12), визирь +(13), толстяк (12) — расхождение будет повторяться на каждом. + +**Что сделать:** свести к одному набору функций над `Char`, оставив +параметрами ровно то, что различается в оригинале: +- окно (`loadkid`/`loadshad` — уже есть); +- набор атласов и таблица кадров (`kid*` / `GUARD` / `SKEL` — уже + выбирается в `pop_guard_load`); +- слот heal-прямоугольников (у каждого персонажа свой, по страницам + дабл-буфера); +- тип объекта для порядка (Kid / тень / страж). + +**Не портированы вовсе и относятся сюда же:** `stuck_lower`, `redraw_at_char2`, +`clip_char` (см. memory `pop_clip_char_todo`). + +**Чем подтверждать:** падение скелета в пропасть с разных X (сегодняшний +сценарий), бой стража на ур. 1-2 без регрессий, `tests-host` зелёные. + +### L3-PASS. Приёмка уровня 3 — обход всех комнат + +Как L1-PASS/L2-PASS: сквозной проход руками плюс обход комнат читом ROOMNAV. +Осмысленна ТОЛЬКО после L3-CHOMP и L3-SKEL — без них уровень заведомо +неполон, и половина наблюдений будет «механики нет». + +Что уже снято и пригодится (справка ниже): комнаты **23 и 24 недостижимы** и +полностью пусты — баги в них не в приоритете; **чекпойнт** уровня 3 сделан +([L3-CHKP](TASKS_CLOSED.md#l3-chkp)), его тоже надо потрогать вживую: уйти +влево из комнаты 7, умереть, проверить респавн в комнате 2 и снятую +loose-плиту (7, кол 4, ряд 0). + +### Справка: связность комнат уровней 1–3 (снято 2026-08-05) + +Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет +соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись +13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый, +в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии. + +| уровень | старт | недостижимы | признак | +|---------|-------|-------------|---------| +| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет | +| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы | +| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** | + +``` +ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7 + 23 R→22, у 22 L=4 | 24 R→7, у 7 L=2 + | 24 U→16, у 16 D=0 + на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty +``` + +То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в +брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот +же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю +колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов +там низкий, в игре их не видно. + +**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная +связь — вверх, откуда Кид падает): + +``` +к.10 U→4 пики(2,2) + пол — падение из комнаты 4 +к.14 U→21 шахта 2 тайла шириной, дно = обломки +к.17 U→15 то же +``` + +Это не баги данных: в 14/17 попадают только падением насмерть, а из 10 +(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать +«застрял» багом. + +--- + +## P1 — берётся в любой момент + +### L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01) + +Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на +логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою +`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()` += 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости +эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою. +Условие «делать ПОСЛЕ CLIP-1» снято — CLIP-1 закрыт. Проверка — +секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз». + +### TUNE-1. Параметры движка — в конфиг, а не в код + +**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном +заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки +(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`). +Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а +не константой по месту. + +**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без +пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под +моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа, +комментарии `;`, неизвестные ключи игнорировать), файл необязателен — +нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`, +`[debug]`, `[enhancements]`. + +**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) + +`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его +(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и +опций — часть задачи: у него уже разложены по группам стартовые +HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало, +чекпойнт), тайминги ворот и пик, скорости, а отдельной группой — +`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже +портировали). Брать всё подряд не надо: переносим по мере того, как +константа реально понадобилась в игре. + +**Оговорка по памяти.** Парсер и таблица параметров — холодный код, +исполняется один раз при старте: кандидат в банк, а не в резидент W1. + +### MEM. Следующий шаг разгрузки W1/W2 + +`pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча +180 Б → 2298 Б; после снижения `--max-allocs` — 2751 Б). Следующий кандидат +по тому же критерию (**не размер, а частота вызова и отсутствие горячих +банк→банк переходов**) — **расщепление `pop_kid.c`**: холодная половина +(загрузка страниц спрайтов, `pop_kid_load`) в банк, движок кадров +(`load_frame`/`play_seq`, 2×/кадр) оставить в резиденте. + +Брать по факту нехватки места, не заранее. Таблица резидентного кода по +модулям и разбор, почему `pop_level.c` в банк НЕЛЬЗЯ, — в +[`TASKS_CLOSED.md`](TASKS_CLOSED.md#mem-bank5). + +--- + +## Отложено осознанно (не брать, пока не появится причина) + +- **KBD-1, остаток** — «иногда при зажатом Shift стрелка всё-таки + пропускается» (ощущение пользователя, не замер: счётчики на 35 нажатиях + подряд потерь не показывают). Решение пользователя 2026-08-01: отложено до + финальной полировки. Где именно осталась дыра и что делать, если вернёмся, + — в [`TASKS_CLOSED.md`](TASKS_CLOSED.md#kbd-1) (там же весь протокол + замеров и список того, что делать НЕЛЬЗЯ). +- **Quickload (Shift+F9) и остальные читы SDLPoP** — оценка сделана + ([DBG-CHEATS](TASKS_CLOSED.md#dbg-cheats)), код не написан. Самое ценное и + самое дорогое: сериализация `Char` + `room_modif` всех комнат + trob'ов + + стражей (`levels_plan.md` §4), зато даёт воспроизводимый регресс «вот кадр, + где баг» вместо ручной подгонки позы. +- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует. +- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6. +- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти + бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается. +- **Отключение мыши на время игры** и **замена PRNG** — + `../docs/ideas_backlog.md` (оба дают доли процента кадра). +- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость + транзиентная; разбор в [`bug_closed.md`](bug_closed.md). diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md index 2316914..23b5405 100644 --- a/applications/PoP/roomtest/bug_closed.md +++ b/applications/PoP/roomtest/bug_closed.md @@ -2,7 +2,9 @@ Сюда переезжает всё, что **закрыто**: подтверждённые фиксы, снятые диагнозы, осознанные решения «не делать». Открытые баги — в -[`bug_list.md`](bug_list.md), текущие задачи — в [`TASKS.md`](TASKS.md). +[`bug_list.md`](bug_list.md), текущие задачи — в +[`TASKS_OPEN.md`](TASKS_OPEN.md), закрытые задачи с протоколами — в +[`TASKS_CLOSED.md`](TASKS_CLOSED.md). Файл существует не ради истории как таковой: половина записей ниже — это разбор КОРНЯ (odd-pixel арифметика `char_x`, подстановка тайла нажатой @@ -11,6 +13,127 @@ --- + +## BUG-GUARD-COLOR-1. Страж и его полоса HP — всегда одного цвета — **ЗАКРЫТ 2026-08-07** + +**Симптом (пользователь, прогон уровня 2).** Комната 4: страж не того цвета, +что в SDLPoP, и полоса его HP тоже. + +**Корень.** Оригинал держит ОДИН набор спрайтов стража и подменяет 16 цветов +палитры: `redraw_screen` (seg003:255) зовёт `set_chtab_palette(chtab_5_guard, +&guard_palettes[0x30*curr_guard_color − 0x30], 16)` ПЕРЕД отрисовкой комнаты, +где `curr_guard_color = level.guards_color[room−1] & 0x0F` (enter_guard, +seg002:184), а `guard_palettes` — ресурс 10 из `PRINCE.DAT` (7 палитр × 16 +цветов, 6-битные каналы). У нас `pop_pack_guard.py` брал `COLOR = 2` +константой (цвет стражей уровня 1) и запекал одну палитру в слоты +`0x90..0x9F`. Полоса HP рисуется тем же атласом — отдельного бага не было. + +**Фикс.** +1. `pop_pack_guard.py` выгружает ВСЕ 7 палитр в `pop_guard_pal.h` + (7 × 16 × 4 = 448 Б, записи `B,G,R,0` — формат `gfx_pal_load`); + каналы масштабируются `scale6to8`, как в `pop_pack_kid` для `kid.pal`. +2. `pop_guard_set_palette(color)` (`pop_gdraw.c`) заливает 16 слотов в ОБЕ + палитры дабл-буфера; `color == 0` — не трогать (так и оригинал). +3. Зовётся из `pop_guard_enter` сразу после чтения данных комнаты, то есть + ДО отрисовки — как `redraw_screen`. `pop_level_guard` цвет отдавал уже + давно, его просто никто не использовал. + +**Грабли, стоившие итерации (записать на будущее).** Первая версия передавала +`gfx_pal_load` указатель прямо на таблицу — а таблица лежит в rodata +БАНКОВОГО модуля, то есть по 0xC000+. `gfx_pal_load` отдаёт указатель в BIOS +(`$A4` через `rst #0x08`), а BIOS читает только `#4000-#BFFF` (корневой +CLAUDE.md). BIOS забирал мусор с чужой страницы, палитра уезжала в тёмное и +страж становился НЕВИДИМЫМ. Лечится копией записи в локальный буфер — стек +гарантированно в W2. + +**Проверено в MAME (уровень 2, ROOMNAV):** + +| комната | цвет из уровня | что на экране | +|---------|----------------|---------------| +| 11 | 1 | страж сине-фиолетовый, полоса HP синяя | +| 7 | 3 | страж оранжевый, полоса HP оранжевая | +| ур. 1 | 2 | охра, как было (регрессии нет) | + +Цвета сходятся с таблицей `pop_guard_pal.h`: цвет 1 = (72,145,255), +цвет 3 = (255,80,0), цвет 2 = (170,48,0). + +**Смежное, НЕ входит сюда:** палитра КЛАДКИ уровня 3 (в оригинале зелёная) — +другой механизм и другой ресурс, заведена отдельной задачей +[L3-COLOR](TASKS_OPEN.md#l3-color). + +--- + + +## BUG-SWORD-GHOST-1. Переход комнаты В БОЮ: Кид прячет меч и дерётся пустой рукой — **ЗАКРЫТ 2026-08-07** + +**Проверено в игре (пользователь):** «вроде проблема не воспроизводится — +всё корректно». Замер на замороженном кадре: оба персонажа в комнате 2, +ряд 1, колонки 4 и 7, `Kid.sword = 2`, `Guard.sword = 2`, луч видимости +прошёл, счётчик уборок меча за прогон — **0**. + +**Симптом.** Кида вытеснили из комнаты 3 в комнату 2 в бою, он спрятал +меч; страж вошёл следом, и дальше Кид «отбивался» без клинка. Зависимость +от расстояния между Кидом и стражем в момент перехода: близко — бой +продолжался нормально, далеко — меч убирался и страж не шёл, а в +промежутке страж приходил, но меч оставался в ножнах. + +**Корень — мнимая стена за краем комнаты, а не боёвка.** Ложным было +`can_guard_see_kid`. Луч видимости (`check_can_guard_see_kid`, seg003:688) +идёт по тайлам между колонками Кида и стража, а колонка считается из `x`: +`floor((x − 7 − 58) / 14)`. На всём диапазоне `x` 0..255 это даёт `col` +от **−5 до 13**, то есть колонка регулярно уходит ЗА комнату. Оригинал +такие колонки резолвит через `find_room_of_tile` (seg006:005D), гуляя по +`roomlinks` на любую глубину; наш `get_tile` знал соседей только на две +колонки (`−2..11`), а дальше отдавал `TILE_WALL`. Луч упирался в эту +мнимую стену, `can_guard_see_kid` падал в 0 — и `control_with_sword` +(ветка «противника не видно») штатно убирал меч. + +Тот же ноль не давал достать меч обратно: `control_standing` зовёт +`draw_sword` только при `can_guard_see_kid >= 2`. Отсюда и «драка без +меча», и зависимость от расстояния — на самом деле не от расстояния, а от +того, вышла ли колонка за кэш. + +**Артефакт (регистратор в памяти, снят с живой сцены).** Брейкпоинт тут +бесполезен — баг ловится руками и редко, поэтому в код были временно +вшиты байты «кто убрал меч и почему оборвался луч»; читались после сцены: + +``` +src = 2 control_with_sword, ветка «противника не видно» +why = 2 луч упёрся в стену +tile = 0x14 = 20 = TILE_WALL +col = 12 ← колонка стража ЗА кэшем (было −2..11) +kid_col = 8 guard_col = 12 +``` + +**Фикс.** `pop_map` кэширует `fg` соседних комнат слева и справа ЦЕЛИКОМ +(по 30 байт, раскладка комнаты) и резолвит `col` от −10 до 19 в реальный +тайл соседа; за этими пределами — по-прежнему стена (у оригинала там +следующий `roomlink`, у нас край кэша). Данные забирает сама +`pop_map_set_edges(left, right, up, down)`, читая уровень — отдельной +копии в приложении нет. Заодно `gate_modif` перестал читать за границу +массива модификаторов: openness ворот на дальних колонках берётся из +`room_modif` СОСЕДА (`pop_gate_modif`, им же пользуется луч). + +Цена: **+48 байт** в W2 (кэш 6+6 → 30+30), код W1 даже уменьшился. +Комнаты сверху/снизу оставлены как были (`above_fg` / `below_fg`): по +вертикали `curr_row` за −1..3 не выходит, мнимых стен там не возникает. + +Попутно вернули строку оригинала, потерянную при порте: `control_with_sword` +сбрасывает `holding_sword` для живого Кида (seg005:980) — от неё зависит +индикатор HP стража. + +**Регресс:** `tests-host` — все 5 наборов зелёные, характеризационные +трассы Кида не сдвинулись (`[phys] ok: 1723`). Новый набор проверок +`char_tiles_resolve_across_rooms` в `t_char` держит резолв колонок +−11..20 в тайлы соседей и стену на краю кэша — чтобы кэш нельзя было +молча сузить обратно. + +**Почему в SDLPoP не воспроизводилось** (пользователь пробовал): там этой +границы просто нет — `find_room_of_tile` уходит по `roomlinks` сколько +нужно, и луч никогда не встречает мнимой стены. + +--- + ## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ЗАКРЫТ 2026-08-05** **Проверено в игре (пользователь):** «зацеп работает». Физика была верна с @@ -404,6 +527,7 @@ K3; K2b просто запускает run-jump». Полировкой это --- + ## BUG-FALL-SWORD-1. Отход с мечом в провал: не та последовательность падения — **ЗАКРЫТ 2026-08-04** **Симптом (приёмка уровня 2, комната 4).** Кид с вынутым мечом отступает @@ -550,6 +674,7 @@ pop_gframe (кадр СТРАЖА, image 17): dx=-1 weight_x=8 --- + ## BUG-LAND-SWORD-1. Кид навсегда застревает в приседе после падения с мечом — **ЗАКРЫТ 2026-08-04** **Симптом (приёмка уровня 2).** Страж ударил Кида, Кид потерял HP, @@ -1521,3 +1646,164 @@ dir_56_none`. То есть стражи — такая же часть дан `../docs/room_model_plan.md`): держать `Kid.room` отдельно от `drawn_room` и рисовать со смещением ±140, как `xpos_in_drawn_room` (seg004:0405). Пока поводов для этого нет — заводить обратно только по живому наблюдению. + +--- + +# Прогон всех комнат уровней 1 и 2 (пользователь, 2026-08-07) — ЗАКРЫЛ ТРИ РЕВИЗИИ РАЗОМ + +Результат прогона: **крупных багов нет**. Тем самым закрыты и переехали сюда +из `bug_list.md` три накопившихся хвоста — чек-листы ручной перепроверки +фиксов, таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений прогона +2026-08-03. Всё, что с прогона 2026-08-07 осталось открытым, — три записи +уровня 2 в [`bug_list.md`](bug_list.md) (BUG-GUARD-COLOR-1, +BUG-GUARD-SPLASH-1, BUG-CHEAT-FIGHT-1); ни одна из них не мешает играть. + +Сырые формулировки пользователя лежат рядом: [`bugs_level1.md`](bugs_level1.md) +и [`bugs_level2.md`](bugs_level2.md). + + +## Ручная перепроверка фиксов (2026-08-03) — ПРОЙДЕНА + +Шесть корней прогона 2026-08-03 были закрыты автоматической проверкой в MAME +(мост `mame-z80`: чтение `_Kid`, потиковые трассы, скриншоты) — этого хватает, +чтобы показать, что конкретный сценарий больше не воспроизводится, но НЕ +хватает, чтобы поймать регрессии в соседней механике. Отсюда список сценариев +ровно в тех формулировках, в которых баги были заведены. Прогон 2026-08-07 +прошёл все комнаты уровней 1 и 2 и ни одного из этих симптомов не показал. + +| # | что проверялось | ожидаемо | +|---|-----------------|----------| +| 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) | +| 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 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки | +| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные | + +Отдельно смотрели **регрессии от порта `check_collisions`** — он трогает всю +горизонтальную коллизию: бамп в стену на бегу и в прыжке, осторожный шаг у +стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме решётки, переходы через +швы (старый BUG-SEAM-PINGPONG). Не всплыло. + + +## Обход всех 24 комнат уровня 1 — ЗАКРЫТ прогоном 2026-08-07 + +Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо +`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой), +Kid ставится на первый пол. Номер комнаты — полосками в верхнем борте: слева +десятки, справа единицы (`||` `||||` = 24). Инструмент остаётся в сборке: +он же нужен для приёмки уровня 3. + +Таблица заполнялась по ходу отладки и осталась незакрытой на 19 строк из 24; +её закрыл ручной прогон всех комнат уровней 1 и 2 (2026-08-07, крупных багов +нет). Ниже — то, что таблица успела зафиксировать: это не «отчёт по +комнатам», а разбор корней, полезный при похожем симптоме. + +| комната | статус | что не так | +|---------|--------|------------| +| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты | +| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` | +| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) | +| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» выше | +| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y−3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` | +| 13, 18, 24 | недостижимы в игре | свойство данных уровня, разбор — «НЕ БАГИ» выше | + + +## Сырые наблюдения (прогон 2026-08-03) → корень + +Одиннадцать формулировок с прогона свелись к шести корням; все шесть закрыты +и проверены в MAME — разборы выше по файлу. + +| # | наблюдение (кратко) | корень | +|---|---------------------|--------| +| 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 | +| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 (поправка — BUG-KBD-5) | +| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | +| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | +| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | + +Вторая волна того же прогона (вечер) дала ещё четыре наблюдения — BUG-KBD-4, +BUG-RESPAWN-2, BUG-DRAWORDER-1, BUG-LOOSE-2, — все закрыты, разборы выше. +Тогда же закрыт как невоспроизводящийся BUG-SEAM-DRAW-1 и заведён +BUG-GATE-PASS-1, который остаётся открытым (ждёт сценария). + +--- + + +## BUG-GUARD-SPLASH-1. Нет «брызг» при попадании по стражу — **ЗАКРЫТ 2026-08-07** + +**Как было заведено (пользователь, 2026-08-07).** При попадании по стражу +должен рисоваться сплеш («звёздочка»), чтобы игрок видел, что удар дошёл до +цели, не переводя взгляд на полосу HP. У Кида такое есть, у стража — нет. + +**Корень.** `draw_hurt_splash` (seg006:16CE) в оригинале зовётся ИЗ ДВУХ +мест: `draw_kid` при `hitp_delta < 0` (seg008:1643) и `draw_guard` при +`guardhp_delta < 0` (seg008:1654). У нас была портирована только первая +половина — `kid_draw_splash()`; для стража вызова не было вовсе, хотя +`guardhp_delta` считался и уже использовался. + +**Фикс** (`pop_guard.c`, `pop_gdraw.c`, `roomtest.c`): + +- флаг `pop_guard_hurt` — симметрия `pop_kid_hurt`: ставится в + `pop_do_delta_hp`, когда `guardhp_delta < 0`, снимается главным циклом + после отрисовки (стража могло не оказаться в отрисованной комнате — + тогда брызги просто пропадают, как в оригинале); +- отрисовка — ВНУТРИ `pop_guard_draw`, между спрайтом стража и клинком: + `draw_guard` кладёт брызги в objtable сразу после стража и ДО клинка, а + порядок записей задаёт порядок рисования. Заодно это решает fore-слой: + `pop_fore_over_char` в конце той же функции накрывает и брызги; +- спрайт — `chtab_5_guard` `obj_id = 1`, то есть **image 1 нашего атласа + стража** (`res752.png`, звезда 28×26): у `add_midtable` аргумент + `obj_id + 1`, а `get_image` вычитает единицу обратно — тот же off-by-one, + что расписан в шапке `pop_pack_guard.py`. У Кида это image 218; +- смещения — три ветки оригинала: кадр 178 (chomped) брызг не даёт вовсе; + 185 и 106..110 (смерть/падение) — `obj_y + 4`; 177 (пики) — сдвиг НАЗАД + на 5; иначе `obj_y − 11` (у Кида 15: `((charid == kid) << 2) + 11`); +- свой прямоугольник heal по страницам (`qx_l/qvalid`) — брызги живут один + кадр, но стирать их обязана та страница, в которую рисовали. + +**Побочно исправлено:** `pop_kid_hurt` взводился при ЛЮБОЙ ненулевой дельте +HP, поэтому зелье здоровья (+1) давало Киду и брызги, и красную вспышку. +И `draw_kid` (seg008:1643), и `flash_if_hurt` (seg003:785) смотрят именно +`hitp_delta < 0` — условие приведено к оригиналу. + +**Проверено в MAME (2026-08-07)**, мост `mame-z80`, уровень 1, комната 21, +живой страж (HP 3/3): + +``` +bp 51BA ← адрес снятия pop_guard_hurt в main: срабатывает ТОЛЬКО + в кадре удара, то есть уже после отрисовки брызг +key k ← чит «убить стража» = guardhp_delta < 0 +→ state=stop PC=0x51BA, guardhp: curr=0 max=3 delta=0 hurt=1 +bp 8BDF (_gfx_set_visible_page) + out ← домотать до флипа страницы +snap ← звезда нарисована поверх стража +``` + +Следующие два кадра (обе страницы дабл-буфера) — чисто, следов брызг нет. + +Приём на будущее: снятие флага сделано **по факту** (`if (pop_guard_hurt) +pop_guard_hurt = 0;`), а не безусловно — это и экономит запись каждый кадр, +и даёт готовую точку останова для отладки в MAME. Условные брейкпоинты +мост не принимает (`bp ADDR cond` вешает плагин), поэтому точка останова, +срабатывающая сама по себе только в нужном кадре, — рабочий обходной путь. + +**Цена:** `_CODE` 24 179 → 24 209 (+30 Б резидента), BANK4 (`pop_gdraw`) +2407 → 3369 (+962 Б, банк занят на 20.6 %, свободно 13 015 Б). + +**Цвет брызг** зависит от палитры стража (слоты `0x90..0x9F`), поэтому он +изменится вместе с [BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1) — +отдельной работы не требует. diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index d0f8257..be55401 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -4,82 +4,84 @@ переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг, грепни там по симптому. -Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта: -механику сверять с `../SDLPoP/src/` ДО кодинга. +Приоритеты работ — в [`TASKS_OPEN.md`](TASKS_OPEN.md) (закрытые задачи с +протоколами — [`TASKS_CLOSED.md`](TASKS_CLOSED.md)), а не здесь. Правило +проекта: механику сверять с `../SDLPoP/src/` ДО кодинга. -Ревизия списка: **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 (стражи не оживают), -BUG-DRAWORDER-1 (Кид поверх тела стража) и BUG-LOOSE-2 (осколки только от одной -из двух плит). У BUG-DRAWORDER-1 остался артефакт «ноги поверх головы», -сверенный с SDLPoP и записанный в «НЕ БАГИ». Тогда же закрыт как -невоспроизводящийся BUG-SEAM-DRAW-1 (Кид в шве рисуется) и заведён -BUG-GATE-PASS-1 — однократное наблюдение прохода сквозь закрывшуюся решётку, -которое ЖДЁТ надёжного сценария. +**Ревизия списка: 2026-08-07 — прогон ВСЕХ комнат уровней 1 и 2** +(пользователь). Крупных багов нет. Поэтому в [`bug_closed.md`](bug_closed.md) +уехали разом: чек-листы ручной перепроверки фиксов (2026-08-03, обе волны), +таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений того же прогона — +все они закрыты этим проходом. С прогона пришли **три новые записи**, все по +уровню 2 (сырые формулировки — [`bugs_level2.md`](bugs_level2.md)). | ID | что | тип | статус | |----|-----|-----|--------| -| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу | -| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария** (BUG-GATEMOD-1, из-за которого решётка стартовала не в том состоянии, закрыт — перепроверить) | +| [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1) | `+`/`−` в бою с вынутым мечом → Кид теряет управление | Major (чит) | открыт | +| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария**: прогоном 2026-08-07 не воспроизведён | +| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается | | [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт | | [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт | -| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт | -| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается | --- -# Уровень 2 — баги с приёмки (заведено 2026-08-04) +# Уровень 2 — баги с приёмки -Уровень 2 заиграл 2026-08-04 (см. [`TASKS.md`](TASKS.md) запись **L2**). -Пользователь при первом прогоне увидел **несколько багов отрисовки** и -подаст их списком отдельно — раздел заведён под них заранее, чтобы -наблюдения не растеклись по переписке. +Приёмка уровня 2 закрыта ([L2-PASS](TASKS_CLOSED.md#l2-pass)): smoke +2026-08-05 + обход всех комнат 2026-08-07. Карта содержимого уровня (что где +стоит по `res2002.bin`, какие кнопки какие ворота открывают) — там же, по ней +видно, «механика не сработала» это или «так и задумано». -**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md#bug-loose-3) — чёрный -бар под упавшей плитой-потолком (запечка восстанавливала не всё, что стёрла). +**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md) — чёрный бар под +упавшей плитой-потолком; [BUG-GUARD-DEAF-1](bug_closed.md) — страж не +оборачивался на вернувшегося Кида; +[BUG-GUARD-SPLASH-1](bug_closed.md#bug-guard-splash-1) — «брызги» при +попадании по стражу; [BUG-SWORD-GHOST-1](bug_closed.md#bug-sword-ghost-1) — +меч, спрятанный посреди боя после перехода комнаты (корень — мнимая стена за +краем комнаты в `get_tile`; кэш соседей расширен до полных комнат); +[BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1) — цвет стража и его +полосы HP теперь берётся из `guards_color` уровня (2026-08-07). -**Порядок работы по уровню 2 (2026-08-05):** наблюдения СНАЧАЛА копятся -здесь целиком — фиксы берём, когда уровень пройден до конца. Причина: -косметика отрисовки почти всегда сводится к общему корню (ср. волну -BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит -чинить один и тот же корень трижды. +Ниже — то, что осталось открытым после прогона 2026-08-07. -Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие -кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass), -запись **L2-PASS**: по ней видно, «механика не сработала» это или -«так и задумано». + +## BUG-CHEAT-FIGHT-1. Чит `+`/`−` в бою: Кид остаётся в режиме боя и теряет управление -Перед заведением записи — грепнуть [`bug_closed.md`](bug_closed.md) по -симптому: почти вся косметика окклюзии уровня 1 уже разбиралась -(BUG-CEIL-1/2/3, BUG-OCCL-1, BUG-DOOR-CLIP, BUG-DRAWORDER-1), и уровень 2 -вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9) -и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый. +**Наблюдение (пользователь, 2026-08-07).** Если нажать `+`/`−` (ROOMNAV, +переход по комнатам) в момент, когда Кид вытащил меч для битвы, — в новой +комнате Кид не управляется: нажатия стрелок игнорируются. - -# Уровень 3 — баги с приёмки (заведено 2026-08-05) +**Корень (прочитан по коду, сверен с seg005).** Чит ROOMNAV +(`roomtest.c:576`) телепортирует и сбрасывает позу и HP — +`enter_room()`, `kid_init(SEQ_STAND, …)`, `pop_kid_hp_reset()`, — но **не +трогает состояние боя**: `Kid.sword` остаётся `SWORD_2_DRAWN`. Диспетчер +`pop_control` (`pop_ctrl.c:578`) при вынутом мече уходит в +`control_with_sword`, а там единственный выход из режима боя — -Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два -из них — **не баги, а неначатые задачи**, и записаны как таковые: +```c +if (Char.frame == FRAME_171_STAND_WITH_SWORD) { /* seg005:987 */ + Char.sword = SWORD_0_SHEATHED; + seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); +} +``` -| наблюдение | что это на самом деле | -|------------|------------------------| -| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта | -| к.10: скелет не оживает | [L3-SKEL](TASKS.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован. **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 | +Стража в новой комнате нет (`can_guard_see_kid` = 0), кадр после +`kid_init(SEQ_STAND)` — обычная стойка, а не 171, поэтому не срабатывает ни +`swordfight()`, ни ветка «убрать меч»: `control_with_sword` каждый кадр +не делает НИЧЕГО, и ввод не доходит до движения. Заклинивание вечное. -Остальные два были настоящими багами и **закрыты в тот же день** — разбор -корней в [`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** (флаги -коллизий считались для одного ряда вместо трёх — бамп терялся на кадре -смены ряда) и **BUG-SEAM-WEDGE-1** (сосед угла (2,0) безусловно считался -стеной). +Это баг **нашего чита**, а не порта: в оригинале телепорта между комнатами +нет, и в режим боя без соперника попасть нечем. + +**Как чинить.** В ветке ROOMNAV телепорт трактовать как выход из боя (то же +самое, что делает `pop_start_level`): `Kid.sword = SWORD_0_SHEATHED`, +`holding_sword = 0`, сбросить `offguard`/`guard_refrac` и состояние стража +(`pop_guard_reset()` вызывается по входу в комнату — проверить, что он +обнуляет `Opp`). Меч в инвентаре (`pop_have_sword`) при этом НЕ терять. +Второй кандидат на ту же болезнь — чит `K` (убить стража) в момент, когда +Кид в стойке с мечом, но не в кадре 171: там оригинал сам доводит до 171 +через `swordfight`, так что проверить сценарием, а не менять вслепую. ## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ @@ -124,28 +126,13 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б `animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked` сверены с seg006/seg007 построчно и совпадают дословно. -**Значит расходится ТАЙМИНГ, а не логика:** у нас пики успевают полностью -выйти ДО того, как Кид на них вбежит, и он проскакивает мимо смертельного -окна 1..4. Триггер — `check_spike_below` (seg006:1199), и у нас он берёт -габарит спрайта из `kid_fp` — метрик ПОСЛЕДНЕГО отрисованного кадра, а -оригинал берёт `char_x_left/right` ТЕКУЩЕГО кадра (`set_char_collision`). -Это первый подозреваемый; второй — момент вызова `process_trobs` -относительно `play_frame` (сколько кадров модификатор успевает пройти, -пока Кид добегает). - -**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом) — оно и уточняет -гипотезу.** Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и -«спрятались не все — часть артефактов осталась». Скриншоты рядом: +**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом).** Кид уронил +плиту-потолок и спрыгнул вниз; пики выдвинулись и «спрятались не все — часть +артефактов осталась». Скриншоты рядом: [наш](bugscreens/l2-r6-spikes-ours.png) и [SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ. -Это стыкуется с замером: пока Кид стоит рядом, `check_spike_below` каждый -кадр зовёт `start_anim_spike`, а тот при отрицательном модификаторе -переставляет его обратно в 0x8F — то есть пики у нас «залипают выдвинутыми» -и отсчёт до уборки не доходит. В оригинале в той же позе они убраны, -значит его `check_spike_below` эту колонку УЖЕ не задевает. - **ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер 2026-08-05, MAME, watchpoint на `room_modif[13]` комнаты 6).** Чистый пробег по УБРАННЫМ пикам убивает штатно: @@ -184,45 +171,21 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б --- + +# Уровень 3 — не баги, а неначатые задачи - -# Ручная перепроверка фиксов (2026-08-03) +Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два +были настоящими багами и **закрыты в тот же день** (разбор — в +[`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** и **BUG-SEAM-WEDGE-1**). +Оставшиеся два — не баги, а неначатые задачи: -Шесть корней закрыты автоматической проверкой в MAME (мост `mame-z80`: -чтение `_Kid`, потиковые трассы, скриншоты) — этого достаточно, чтобы -показать, что конкретный сценарий больше не воспроизводится, но НЕ -достаточно, чтобы поймать регрессии в соседней механике. Ниже — сценарии -ровно в тех формулировках, в которых баги были заведены. +| наблюдение | что это на самом деле | +|------------|------------------------| +| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS_OPEN.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта | +| к.10: скелет не оживает | [L3-SKEL](TASKS_OPEN.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован. **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 | -Собрано и лежит в образе (`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_closed.md`) | -| 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, вечер) - -| # | что проверить | ожидаемо | -|---|---------------|----------| -| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки | -| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные | - -Отдельно стоит посмотреть **на регрессии от порта `check_collisions`** — он -трогает всю горизонтальную коллизию: бамп в стену на бегу и в прыжке, -осторожный шаг у стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме -решётки, переходы через швы (старый BUG-SEAM-PINGPONG). +Приёмки уровня 3 (полного обхода комнат) ещё не было — она осмысленна только +после L3-CHOMP/L3-SKEL. --- @@ -231,11 +194,14 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б ## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ -**Статус: наблюдался один раз, воспроизвести повторно НЕ удалось.** Заведён, -чтобы наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как -«не баг», пока нет надёжного сценария. +**Статус: наблюдался один раз (2026-08-03), воспроизвести не удалось ни +тогда, ни прогоном всех комнат уровня 1 (2026-08-07).** Заведён, чтобы +наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как +«не баг», пока нет надёжного сценария. Оговорка: BUG-GATEMOD-1, из-за +которого решётка стартовала не в том состоянии, с тех пор закрыт, и +наблюдение могло быть его следствием. -**Что наблюдалось (2026-08-03).** Комната 5: Кид стоял НА тайле решётки (0,9) +**Что наблюдалось.** Комната 5: Кид стоял НА тайле решётки (0,9) и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1 и упал на (1,1). @@ -266,7 +232,8 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б 3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать. Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2** -продолжить), затем чтение `_Kid` из отладчика MAME. +продолжить), затем чтение `_Kid` из отладчика MAME; подгонка позы по пикселю — +читы `[`/`]`. **Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке, которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в @@ -275,6 +242,8 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б `can_bump_into_gate()`: если он берётся не от того кадра, решётка может перестать считаться препятствием раньше времени. +--- + # Оптимизация отрисовки (записано 2026-07-29) Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к @@ -328,51 +297,8 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б heal'а нет, стирать пики нечем, редрой не нужен. **Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно -там, где тапают Shift+стрелку (см. остаток KBD-1 в [`TASKS.md`](TASKS.md)). - ---- - -# Обход всех 24 комнат уровня 1 — поиск багов отрисовки - -Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо -`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой), -Kid ставится на первый пол, `pop_trob_reset()` возвращает пики/ворота в -исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа -единицы (`||` `||||` = 24). Убирается закомментированием одной строки. - -Комнаты **13, 18, 24 недостижимы** в обычной игре — это свойство данных -уровня, разбор в [`bug_closed.md`](bug_closed.md) («НЕ БАГИ»). Приоритет -багов в них низкий. - -Таблица заполнена на 5 строк из 24 — доделать в рамках L1-TRIAGE -([`TASKS.md`](TASKS.md)). - -| комната | статус | что не так | -|---------|--------|------------| -| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты | -| 2 | | | -| 3 | | | -| 4 | | | -| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` | -| 6 | | | -| 7 | | | -| 8 | | | -| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) | -| 10 | | | -| 11 | | | -| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» в `bug_closed.md` | -| 13 | недостижима в игре | | -| 14 | | | -| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y−3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` | -| 16 | | | -| 17 | | | -| 18 | недостижима в игре | | -| 19 | | | -| 20 | | | -| 21 | | | -| 22 | | | -| 23 | | | -| 24 | недостижима в игре | | +там, где тапают Shift+стрелку (остаток KBD-1 отложен, см. +[`TASKS_CLOSED.md`](TASKS_CLOSED.md#kbd-1)). --- @@ -382,32 +308,14 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ (эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в сторону = разворот (как в оригинале safe_step), движение со второго. - Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle), - **S** — выдать меч. + **S** — выдать меч, **Shift+L** — следующий уровень, **`[`/`]`** — сдвиг + Кида на пиксель по X. - Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos). - **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и `S`. Все они со временем съедутся в общий блок читов, разрешаемый в настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`). - - ---- - - -## Сырые наблюдения (прогон 2026-08-03) → корень - -Формулировки с прогона, как записаны. Закрытые корни — в -[`bug_closed.md`](bug_closed.md). - -| # | наблюдение (кратко) | корень | статус | -|---|---------------------|--------|--------| -| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт | -| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт | -| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт | -| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт | -| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт | -| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт | -| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) | -| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) | -| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт | -| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт | -| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт | + Известный баг этого чита — [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1). +- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство + данных уровня (разбор — «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)); + приоритет багов в них низкий. Аналогично 23/24 на уровне 3. diff --git a/applications/PoP/roomtest/bugs_level2.md b/applications/PoP/roomtest/bugs_level2.md new file mode 100644 index 0000000..4a0ce5b --- /dev/null +++ b/applications/PoP/roomtest/bugs_level2.md @@ -0,0 +1,10 @@ +Баги/недоработки на уровне 2 + +- комната 4 - бой со стражем - страж другого цвета/другого цвета у него HP (сравнение с SDLPoP) +- комната 4 - бой со стражем - при нанесении урона стражу должен рисоваться сплеш (типа звезды) - что бы игрок + видел что удар дошел до цели без перевода внимания на HP ( + +- баг нашего cheats - если мы нажимаем +/- (переход по комнатам) в момент когда Кид вытащил меч для битвы - + Кид теряет управление (он переключается в режим боя но в новой комнате нету стража - в результате нажатия + кнопок движения игнорятся) + diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index daa5d6a..a7e93dd 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -21,11 +21,13 @@ #include "pop_map.h" #include "pop_geom.h" #include "pop_trob.h" -#include "pop_state.h" /* pop_immortal — чит бессмертия */ +#include "pop_level.h" /* pop_current_level — спецсобытие уровня 3 */ +#include "pop_state.h" /* pop_immortal, pop_droppedout */ #define TILE_MIDX 7 /* середина тайла в координатах персонажа */ #define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */ +#define TILE_SPIKE 2 #define TILE_GATE 4 #define TILE_DOORTOP_FLOOR 7 #define TILE_LOOSE 11 @@ -33,6 +35,10 @@ #define TILE_CHOMPER 18 #define TILE_WALL 20 +/* actions (types.h:334) — нужны ветке «идти ли за упавшим Кидом». */ +#define ACT_HANGCLIMB 2 +#define ACT_HANGSTRAIGHT 6 + /* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и * «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его * через границу банка дороже, чем повторить. */ @@ -107,13 +113,63 @@ void pop_check_can_guard_see_kid(void) __banked if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) { can_guard_see_kid = 1; } else if (tile == TILE_GATE && - pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) { + pop_gate_modif(tile_col, Kid.curr_row) < 112) { can_guard_see_kid = 1; /* ворота подняты не до конца */ } left_pos += TILE_SIZEX; } } + +/* ---- Скелет: спецсобытие подъёма (check_skel, seg002:1042) ---------- * + * На уровне 3 стражей в данных НЕТ ВООБЩЕ (guards_tile пуст во всех 24 + * комнатах) — единственный противник поднимается сам: когда Кид в комнате 1 + * доходит до колонки 2 или 3 при открытой двери уровня, тайл 21 (скелет) + * стирается в пол, а на его месте встаёт персонаж слота Guard с уже вынутым + * мечом. Отсюда и место здесь, рядом с ИИ, а не в enter_guard. */ +#define SKEL_LEVEL 3 +#define SKEL_ROOM 1 +#define SKEL_COL 5 +#define SKEL_ROW 1 +#define SKEL_TRIG_COL1 2 +#define SKEL_TRIG_COL2 3 +#define SKEL_SKILL 2 +#define SKEL_HP 3 +#define TILE_SKELETON 21 + +void pop_check_skel(void) __banked +{ + if (pop_current_level != SKEL_LEVEL) return; + if (Guard.charid != 0) return; /* слот занят (dir_56_none) */ + if (Kid.room != SKEL_ROOM) return; + if (!pop_leveldoor_open) return; + if (Kid.curr_col != SKEL_TRIG_COL1 && Kid.curr_col != SKEL_TRIG_COL2) return; + if (pop_tile_at(SKEL_COL, SKEL_ROW) != TILE_SKELETON) return; + + pop_skel_wake_tile((uint8_t)(SKEL_ROW * 10 + SKEL_COL)); + + /* Поднимаем В ОКНЕ Char — как оригинал (он же и saveshad'ит). */ + pop_loadshad(); + Char.room = Kid.room; + Char.curr_row = SKEL_ROW; + Char.y = (uint8_t)pop_y_land[SKEL_ROW + 1]; + Char.curr_col = SKEL_COL; + Char.x = (uint8_t)(pop_x_bump[SKEL_COL + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX); + Char.direction = -1; /* dir_FF_left */ + Char.alive = -1; + Char.sword = SWORD_2_DRAWN; + Char.charid = CHARID_4_SKELETON; + Char.fall_x = Char.fall_y = 0; + pop_char_set_seq(SEQ_88_SKEL_WAKE_UP); + play_seq(); + pop_saveshad(); + + guard_skill = SKEL_SKILL; + guardhp_max = guardhp_curr = SKEL_HP; + guardhp_delta = 0; + is_guard_notice = guard_refrac = 0; +} + /* ---- Синтетический ввод (seg002:0706..) --------------------------- */ static void move_0_nothing(void) { @@ -281,6 +337,35 @@ static void autocontrol_guard_kid_far(void) move_2_backward(); } +/* guard_follows_kid_down (seg002:09F8): Кида спихнули с уступа (droppedout), + * страж его больше не видит — идти ли следом вниз? Условия оригинала, все + * «не идти»: впереди стена; ИЛИ впереди не пол и при этом внизу пика, + * loose-плита, стена или пустота; ИЛИ Кид не ровно на ряд ниже. + * + * Тайлы читаются через pop_tile_at — это банк 1 → банк 3, то есть трамплин. + * Здесь это не жаль: ветка срабатывает только в кадре, когда Кид сорвался + * из боя, а не каждый кадр (её гейтят can_guard_see_kid == 0 и droppedout). + * + * Не идём — сбросить droppedout и попятиться; идём — шаг вперёд, дальше + * персонажа уводит вниз обычная физика (та же, что у Кида). */ +static void guard_follows_kid_down(void) +{ + uint8_t below, front; + if (Opp.action == ACT_HANGCLIMB || Opp.action == ACT_HANGSTRAIGHT) return; + front = tile_infront(1); + below = pop_tile_at((int8_t)(Char.curr_col + (Char.direction < 0 ? -1 : 1)), + (int8_t)(Char.curr_row + 1)); + if (front == TILE_WALL || + (!tile_is_floor(front) && + (below == TILE_SPIKE || below == TILE_LOOSE || below == TILE_WALL || + !tile_is_floor(below) || Char.curr_row + 1 != Opp.curr_row))) { + pop_droppedout = 0; + move_2_backward(); + } else { + move_1_forward(); + } +} + /* autocontrol_guard_active (seg002:737). */ static void autocontrol_guard_active(void) { @@ -293,9 +378,11 @@ static void autocontrol_guard_active(void) return; if (can_guard_see_kid == 0) { - /* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе - * с физикой стража; скелета (charid 4) ветка тоже не берёт. */ - if (Char.charid != CHARID_4_SKELETON) move_down_back(); + /* seg002:740: Кид пропал из виду. Если его СПИХНУЛИ (droppedout) — + * решаем, идти ли за ним вниз; иначе просто отступаем. Скелет не + * отступает никогда (у него нет этой ветки в оригинале). */ + if (pop_droppedout) guard_follows_kid_down(); + else if (Char.charid != CHARID_4_SKELETON) move_down_back(); return; } @@ -339,6 +426,9 @@ static void autocontrol_opponent(void) if (justblocked) --justblocked; if (kid_sword_strike) --kid_sword_strike; if (guard_refrac) --guard_refrac; + /* autocontrol_skeleton (seg002:685): у скелета меч вынут ВСЕГДА — он не + * бывает «неактивным», поэтому ветка inactive ему недоступна. */ + if (Char.charid == CHARID_4_SKELETON) Char.sword = SWORD_2_DRAWN; if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive(); else autocontrol_guard_active(); } @@ -355,22 +445,6 @@ static void autocontrol_opponent(void) static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] = { 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0}; -/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту. - * Возврат 1 — HP кончились. Чит бессмертия ловим здесь: анимацию удара - * Кид получает, а HP не теряет. */ -static uint8_t take_hp(uint8_t count) -{ - if (Char.charid == CHARID_0_KID) { - if (pop_immortal) return 0; - if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; } - hitp_delta = (int8_t)-(int8_t)count; - } else { - if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; } - guardhp_delta = (int8_t)-(int8_t)count; - } - return 0; -} - /* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу. * Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр * «получил удар». @@ -392,9 +466,9 @@ static void hurt_by_sword(void) return; } if (Char.sword != SWORD_2_DRAWN) { - take_hp(100); + pop_take_hp(100); pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); - } else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) { + } else if (Char.charid != CHARID_4_SKELETON && pop_take_hp(1)) { pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */ } else { pop_char_set_seq(SEQ_74_HIT_BY_SWORD); diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index b644299..6f54494 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -1700,12 +1700,56 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, * (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий * порт redraw_at_char2, а не догадки сейчас. */ void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h, - int8_t dir, uint8_t sword) __banked + int8_t dir, uint8_t sword, + int ex, int ey, uint16_t ew, uint16_t eh, + uint8_t action, int8_t ccol, int8_t crow) __banked { int8_t r, c; fclip_on = 1; /* окно = прямоугольник спрайта */ char_footprint(obj_x, obj_y, w, h, dir, sword); + /* Футпринт считается по КАДРУ персонажа (как char_x_left/right, + * seg006:1021), но рядом с ним рисуются ещё клинок и «брызги» удара — + * они уходят ВПЕРЁД-ВВЕРХ и запросто попадают в соседнюю колонку или + * ряд выше. Оригинал этим не страдает: он помечает тайлы на перерисовку + * и рисует foretable целиком, а у нас проход сужен окном ради скорости + * (см. pop_fore_layer_cost). Поэтому расширяем перебор объединённым + * ЭКРАННЫМ прямоугольником «персонаж + клинок + брызги»: иначе кусок + * меча или звёздочка удара остаются поверх кладки. */ + if (ew && eh) { + int8_t c2, r2; + for (c2 = 0; c2 <= 9; c2++) { + int x0 = COL_XH[c2] * 8; + if (x0 + 40 > ex && x0 < ex + (int)ew) { + if (c2 < fp_cL) fp_cL = c2; + if (c2 > fp_cR) fp_cR = c2; + } + } + for (r2 = 0; r2 <= 2; r2++) { + int y0 = POP_YOFF + 63 * r2; + if (y0 + 70 > ey && y0 - 8 < ey + (int)eh) { + if (r2 < fp_rT) fp_rT = r2; + if (r2 > fp_rB) fp_rB = r2; + } + } + } gfx_set_bank(GFX_BANK_SPRITE); + /* Порядок midtable — общий для ЛЮБОГО Char (redraw_needed_tiles, + * seg008:1B06): персонаж вставляется в очередь при обработке СВОЕГО + * тайла, поэтому тайлы, идущие в обходе позже, ложатся ПОВЕРХ него. У + * Кида это уже эмулируется other_overlay_tile, а у соперника прохода не + * было вовсе — падающий скелет оказывался поверх верхней грани пола + * соседней колонки. Гейт по action тот же, что у Кида: вис/полёт/ + * падение/подъём (2/3/4/6). */ + obj_row = crow; + obj_col = ccol; + if (action == 1) { obj_row = fp_rB; obj_col = fp_cL; } + if (action == 2 || action == 3 || action == 4 || action == 6) { + obj_col--; + for (c = fp_cR; c >= fp_cL; c--) { + if (action != 2) other_overlay_tile(fp_rB, c); + if (fp_rT != fp_rB) other_overlay_tile(fp_rT, c); + } + } for (r = fp_rT; r <= fp_rB; r++) for (c = fp_cL; c <= fp_cR; c++) fore_tile(r, c); diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index dab1ebf..254ab50 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -79,8 +79,14 @@ void pop_fore_set_clip(int x, int y, int w, int h) __banked; /* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому * вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в * кадр, других вызывающих нет — цена одного трамплина здесь не видна. */ +/* ex/ey/ew/eh — ОБЪЕДИНЁННЫЙ экранный прямоугольник «персонаж + клинок + + * брызги» (тот же, что уходит в pop_fore_set_clip): перебор передних тайлов + * идёт по нему, иначе меч и звёздочка удара остаются поверх кладки. + * ew == 0 — расширения нет (только габарит кадра). */ void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h, - int8_t dir, uint8_t sword) __banked; + int8_t dir, uint8_t sword, + int ex, int ey, uint16_t ew, uint16_t eh, + uint8_t action, int8_t ccol, int8_t crow) __banked; /* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ * (draw_other_overlay/floor_overlay/полоса потолка): он выходит за diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index 7922b79..24987ed 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -17,6 +17,7 @@ */ #include #include +#include "pop_state.h" #include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */ #include "pop_kid.h" #include "pop_map.h" @@ -539,6 +540,8 @@ static void control_with_sword(void) } } /* Не видит / далеко. */ + if (Char.charid == CHARID_0_KID && Char.alive < 0) + holding_sword = 0; /* seg005:980 — индикатор HP стража */ if (Char.charid < CHARID_2_GUARD) { if (Char.frame == FRAME_171_STAND_WITH_SWORD) { Char.sword = SWORD_0_SHEATHED; diff --git a/applications/PoP/roomtest/pop_gdraw.c b/applications/PoP/roomtest/pop_gdraw.c index 74a595a..5c8a128 100644 --- a/applications/PoP/roomtest/pop_gdraw.c +++ b/applications/PoP/roomtest/pop_gdraw.c @@ -19,7 +19,33 @@ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */ +#include "pop_level.h" /* тип соперника уровня (набор спрайтов) */ #include "kid_data.h" +#include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */ + +/* Цвет стража (set_chtab_palette, seg003:257). Оригинал держит ОДИН набор + * спрайтов и подменяет 16 цветов палитры chtab_5 по level.guards_color — + * так на уровне 2 стражи комнат 4/7/11/15/24 разного цвета (1/3/1/1/6). + * Полоса HP стража рисуется тем же атласом, поэтому меняется заодно. + * color = 0 — палитру не трогаем (оригинал в этом случае set_chtab_palette + * не зовёт: тип уровня не «обычный страж»). + * Грузим В ОБЕ палитры: у страниц дабл-буфера они раздельные. */ +void pop_guard_set_palette(uint8_t color) __banked +{ + /* Таблица — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а + * gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS + * читает только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем + * запись в локальный буфер — стек гарантированно в W2. Без копии BIOS + * забирал мусор с чужой страницы и страж становился невидимым. */ + uint8_t buf[64]; + const uint8_t *p; + uint8_t i; + if (color == 0 || color > POP_GUARD_NPAL) return; + p = pop_guard_pal[color - 1]; + for (i = 0; i < 64; i++) buf[i] = p[i]; + gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); + gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); +} /* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */ #define GUARD_PAGES 5 @@ -39,6 +65,13 @@ static int sx_l[2], sy_l[2]; static uint16_t sw_l[2], sh_l[2]; static uint8_t svalid[2]; +/* Брызги удара — тоже свой прямоугольник, по той же причине: спрайт мелкий и + * уезжает вперёд-вверх от стража. Живут они ОДИН кадр, но стереть их обязана + * та же страница, в которую рисовали (иначе остаток виден через кадр). */ +static int qx_l[2], qy_l[2]; +static uint16_t qw_l[2], qh_l[2]; +static uint8_t qvalid[2]; + /* Ширина спрайта последнего кадра стража — нужна set_char_collision * (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам: * это свойство кадра, а не буфера. Отставание на кадр допустимо — так же @@ -47,16 +80,40 @@ static uint16_t g_last_w; uint16_t pop_guard_fp_width(void) __banked { return g_last_w; } +/* Набор спрайтов соперника выбирает ТИП УРОВНЯ (load_lev_spr, seg000:1092): + * tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, −1 нет}. + * У нас пока два набора: обычный страж (уровни 1-2) и скелет (уровень 3); + * у скелета и кадров меньше (28 против 34), поэтому пятую страницу он не + * грузит. Уровень БЕЗ соперников (type < 0) не грузит ничего. */ int pop_guard_load(void) __banked { - static const char *const names[GUARD_PAGES] = { + static const char *const guard_names[GUARD_PAGES] = { "GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl", "GUARD\\g3.atl", "GUARD\\g4.atl" }; + static const char *const skel_names[GUARD_PAGES] = { + "SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl", + "SKEL\\g3.atl", 0 + }; + const char *const *names; + int8_t type = pop_level_guard_type(pop_current_level); uint8_t i; - for (i = 0; i < GUARD_PAGES; i++) + if (type < 0) return 0; /* соперников на уровне нет */ + names = (type == 2) ? skel_names : guard_names; + for (i = 0; i < GUARD_PAGES; i++) { + if (names[i] == 0) break; if (atlas_load(&gp[i], names[i]) != 0) return -1; + } g_ok = 1; + /* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется + * из res10.bin по guards_color — это делает pop_guard_set_palette при + * входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ + if (type == 2) { + uint8_t buf[64], k; + for (k = 0; k < 64; k++) buf[k] = pop_skel_pal[k]; /* BIOS: буфер в W2 */ + gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); + gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); + } return 0; } @@ -77,7 +134,8 @@ void pop_guard_heal(void) __banked uint8_t p = gfx_get_draw_page() & 1; if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]); if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]); - gvalid[p] = svalid[p] = 0; + if (qvalid[p]) pop_heal_fast(qx_l[p], qy_l[p] + POP_YOFF, qw_l[p], qh_l[p]); + gvalid[p] = svalid[p] = qvalid[p] = 0; } void pop_guard_draw(void) __banked @@ -109,19 +167,93 @@ void pop_guard_draw(void) __banked h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); if (w && h) { int bx = obj_x; + /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507: + * obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за + * него (скелет, падающий в пропасть), обязан обрезаться: ниже поля + * идёт полоса HP, и без клипа спрайт лез прямо на неё. */ + int skip = 0, rows; + top = obj_y - (int)h + 1; flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */ if (flip) bx -= (int)w; - top = obj_y - (int)h + 1; - p = gfx_get_draw_page() & 1; - gfx_set_bank(GFX_BANK_SPRITE); - if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) - gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0); - else - 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; + rows = (int)h; + if (top < 0) { skip = -top; rows -= skip; top = 0; } + if (top + rows > 192) rows = 192 - top; + if (rows > 0) { + p = gfx_get_draw_page() & 1; + gfx_set_bank(GFX_BANK_SPRITE); + if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows)) + gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, + (uint8_t)skip, (uint8_t)rows); + else + gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, skip, rows); + gfx_set_bank(GFX_BANK_NORMAL); + /* heal — ровно по НАРИСОВАННОМУ (обрезанному) прямоугольнику. */ + gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = (uint16_t)rows; + gvalid[p] = 1; + clip_x = bx; clip_y = top; clip_w = w; clip_h = (uint16_t)rows; + } g_last_w = w; /* для char_width_half, см. выше */ - clip_x = bx; clip_y = top; clip_w = w; clip_h = h; + } + /* Брызги удара (draw_hurt_splash, seg006:16CE) — ровно здесь, между + * стражем и клинком: draw_guard кладёт их в objtable СРАЗУ после стража + * и ДО клинка (seg008:22C9), а порядок записей задаёт порядок рисования. + * + * Спрайт — chtab_5_guard `obj_id = 1`, то есть image 1 нашего атласа + * стража (страница 0, индекс 1): у add_midtable аргумент `obj_id + 1`, а + * get_image вычитает единицу обратно (тот же off-by-one, что расписан в + * шапке pop_pack_guard.py). У Кида это image 218 своего chtab. + * + * Смещения от кадра персонажа — по трём веткам оригинала; у стража + * подъём на 11 (у Кида 15: `((charid == kid) << 2) + 11`). */ + if (pop_guard_hurt && Guard.frame != 178 /* chomped: брызг нет */) { + int qx = fp_x, qy = obj_y; + const uint8_t *qimg = (const uint8_t *)atlas_image(&gp[0], 1); + uint16_t qw, qh; + int fw = (Guard.direction < 0) ? -5 : 5; /* obj_dx_forward(5) */ + if (Guard.frame == 185 || /* мёртв */ + (Guard.frame >= 106 && Guard.frame < 111)) { /* падение+посадка */ + qy += 4; + } else if (Guard.frame == 177) { /* напоролся на пики */ + fw = -fw; /* obj_dx_forward(-5) */ + } else { + qy -= 11; + } + qx = (qx + fw) * 8 / 7; /* calc_screen_x_coord */ + gfx_w0_map(gp[0].page); + qw = (uint16_t)(qimg[0] | ((uint16_t)qimg[1] << 8)); + qh = (uint16_t)(qimg[2] | ((uint16_t)qimg[3] << 8)); + if (qw && qh) { + int qtop = qy - (int)qh + 1; + uint8_t qflip = (uint8_t)(Guard.direction >= 0); + int qskip = 0, qrows = (int)qh; /* клип полем 192, как у спрайта */ + if (qflip) qx -= (int)qw; + if (qtop < 0) { qskip = -qtop; qrows -= qskip; qtop = 0; } + if (qtop + qrows > 192) qrows = 192 - qtop; + if (qrows <= 0) goto splash_done; + qh = (uint16_t)qrows; + gfx_set_bank(GFX_BANK_SPRITE); + if (pop_onscreen_cols(qx, qtop + POP_YOFF, qw, qh)) + gfx_blit_cols_part_noclip(qx, qtop + POP_YOFF, qimg, qflip, + (uint8_t)qskip, (uint8_t)qrows); + else + gfx_blit_cols_part(qx, qtop + POP_YOFF, qimg, qflip, qskip, qrows); + gfx_set_bank(GFX_BANK_NORMAL); + p = gfx_get_draw_page() & 1; + qx_l[p] = qx; qy_l[p] = qtop; qw_l[p] = qw; qh_l[p] = qh; + qvalid[p] = 1; + if (!clip_w) { + clip_x = qx; clip_y = qtop; clip_w = qw; clip_h = qh; + } else { + int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h; + if (qx < clip_x) clip_x = qx; + if (qtop < clip_y) clip_y = qtop; + if (qx + (int)qw > x1) x1 = qx + (int)qw; + if (qtop + (int)qh > y1) y1 = qtop + (int)qh; + clip_w = (uint16_t)(x1 - clip_x); + clip_h = (uint16_t)(y1 - clip_y); + } + } +splash_done: ; } /* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable, * seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник @@ -157,7 +289,9 @@ void pop_guard_draw(void) __banked * (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний * столб. */ if (w && h) - pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword); + pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword, + clip_x, clip_y + POP_YOFF, clip_w, clip_h, + Guard.action, Guard.curr_col, Guard.curr_row); } /* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- * diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index 111133c..e767e3d 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -11,11 +11,13 @@ #include "pop_level.h" #include "pop_kid.h" /* Char/play_seq — интерпретатор общий */ #include "pop_map.h" /* hitp_* — HP Кида */ +#include "pop_state.h" /* pop_immortal — чит бессмертия */ #include "kid_data.h" pop_char_t Guard; uint8_t guardhp_curr, guardhp_max; int8_t guardhp_delta; +uint8_t pop_guard_hurt; int8_t can_guard_see_kid; int8_t is_guard_notice; uint8_t guard_skill; @@ -66,6 +68,20 @@ void pop_guard_kill(void) Guard.alive = 0; } +/* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */ +uint8_t pop_take_hp(uint8_t count) +{ + if (Char.charid == CHARID_0_KID) { + if (pop_immortal) return 0; + if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; } + hitp_delta = (int8_t)-(int8_t)count; + } else { + if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; } + guardhp_delta = (int8_t)-(int8_t)count; + } + return 0; +} + /* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить * её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */ void pop_do_delta_hp(void) @@ -76,7 +92,10 @@ void pop_do_delta_hp(void) if (hp < 0) hp = 0; if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max; hitp_curr = (uint8_t)hp; - pop_kid_hurt = 1; /* -> «брызги» поверх Кида */ + /* Брызги и красная вспышка — только на УРОН: и draw_kid (seg008:1643), + * и flash_if_hurt (seg003:785) смотрят именно `hitp_delta < 0`. С + * условием «дельта не ноль» их получало и зелье здоровья (+1). */ + if (hitp_delta < 0) pop_kid_hurt = 1; hitp_delta = 0; } if (guardhp_delta) { @@ -84,10 +103,52 @@ void pop_do_delta_hp(void) if (hp < 0) hp = 0; if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max; guardhp_curr = (uint8_t)hp; + if (guardhp_delta < 0) pop_guard_hurt = 1; /* -> брызги поверх стража */ guardhp_delta = 0; } } +/* check_guard_fallout (seg002:0241): персонаж слота Guard провалился НИЖЕ + * комнаты. Уйти в комнату снизу он не может (у стража нет перехода), и + * оригинал его просто убирает: guards_tile комнаты становится −1, то есть + * стража там больше нет и после возврата. У нас тот же смысл — запомнить + * состояние с tile = 30 («стража в комнате больше нет»). + * + * Ветки тени (падает, только если она в свободном полёте) и скелета (тот + * возрождается в комнате 3) появятся вместе с L3-SKEL — сейчас в слоте + * бывает только обычный страж. + * + * Полосу HP перерисовывать отдельно не надо: pop_hp_draw сравнивает числа + * и сам увидит, что стража не стало. */ +/* Куда и как возрождается упавший СКЕЛЕТ (seg002:252): если под комнатой, + * из которой он выпал, лежит комната 3 — он появляется там снова. Убить + * его нельзя вовсе, падение лишь переносит. */ +#define SKEL_REAPPEAR_ROOM 3 +#define SKEL_REAPPEAR_X 133 +#define SKEL_REAPPEAR_ROW 1 + +void pop_guard_fallout(void) +{ + if (Guard.charid == 0 || Guard.y < 211) return; + if (Guard.charid == CHARID_4_SKELETON) { + uint8_t down = pop_room_link(Guard.room, 3 /* вниз */); + if (down == SKEL_REAPPEAR_ROOM) { + Guard.room = down; + Guard.x = SKEL_REAPPEAR_X; + Guard.curr_row = SKEL_REAPPEAR_ROW; + Guard.direction = 0; /* dir_0_right */ + Guard.alive = -1; + pop_guard_leave(); /* запомнить его в новой комнате */ + return; + } + } + pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0); + Guard.charid = 0; + Guard.direction = 0; + Guard.alive = 0; + guardhp_curr = 0; +} + /* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у * стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где * add_frame = 70 для кадров 102..106 (seg006:535). */ @@ -114,6 +175,60 @@ void pop_guard_leave(void) (uint8_t)(Guard.alive >= 0)); } +static int8_t guard_col_from_x(uint8_t xpos); /* определена ниже */ + +/* ---- Страж ИДЁТ ЗА КИДОМ в соседнюю комнату ------------------------- * + * Порт ветки стража из exit_room (seg002:0340..0382) + follow_guard + * (seg002:039E). Своим ходом персонаж слота Guard из комнаты не уходит + * (его физика гейтится x ∈ [44,211), см. pop_guard_phys_tick), но когда + * комнату меняет КИД, движок может перенести стража следом — именно так в + * оригинале страж из комнаты 3 оказывается в комнате 2 за отступающим + * Кидом. + * + * Условия оригинала, все обязательные: + * - страж есть, ЖИВ и с ВЫНУТЫМ МЕЧОМ (то есть идёт бой); + * - переход ВБОК: вверх и вниз оригинал не пускает никогда (там условия + * `curr_row >= 0` и `curr_row < 3` истинны всегда → leave_guard); + * - в целевой комнате НЕТ своего ЖИВОГО стража (иначе их стало бы два: + * проверка `guards_tile >= 30 || guards_seq_hi != 0` — «пусто или + * труп»); + * - страж прижат к тому краю, в который ушёл Кид: x < 91 при уходе + * влево, x >= 165 при уходе вправо. + * + * side — куда ушёл КИД (0 = left, 1 = right, 2 = up, 3 = down), то есть + * roomleave_result оригинала. Возврат: 1 — страж перенесён в new_room + * (вызывающий НЕ должен звать ни pop_guard_leave, ни pop_guard_enter), + * 0 — остаётся, всё как раньше. */ +uint8_t pop_guard_follow(uint8_t new_room, uint8_t side) +{ + uint8_t tile, color, skill; + int8_t dir; + + if (Guard.charid == 0) return 0; /* dir_56_none */ + if (Guard.alive >= 0 || Guard.sword != SWORD_2_DRAWN) return 0; + if (side > 1) return 0; /* только вбок */ + /* «В целевой комнате уже есть живой страж» = страж по данным есть И это + * не запомненный труп (у трупа сохранена последовательность). */ + if (pop_level_guard(new_room, &tile, &dir, &color, &skill) && + pop_guard_state_seq(new_room) == 0) return 0; + if (side == 0) { if (Guard.x >= 91) return 0; } /* Кид ушёл влево */ + else { if (Guard.x < 165) return 0; } /* Кид ушёл вправо */ + + /* follow_guard: сначала «стража нет» в ОБЕИХ комнатах (в оригинале + * guards_tile = 0xFF). Без этого он раздвоится: в старой комнате его + * поднимут из данных при возврате, а в новой — при следующем входе. */ + pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0); + pop_guard_state_save(new_room, 30, 0, 0, 0, 0, 0); + + /* goto_other_room (seg002:0486) для стража: комната + перенос X через + * шов (влево +140, вправо −140). Ряд при боковом переходе не меняется; + * колонку пересчитываем из новой X, как enter_guard. */ + Guard.room = new_room; + Guard.x = (uint8_t)(side == 0 ? Guard.x + 140 : Guard.x - 140); + Guard.curr_col = guard_col_from_x(Guard.x); + return 1; +} + /* 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) @@ -134,6 +249,10 @@ void pop_guard_enter(uint8_t room) * на таком уровне полезли бы стражи из данных комнат. */ if (pop_level_guard_type(pop_current_level) < 0) return; if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return; + /* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты + * палитры общие на экран, и смена посреди кадра дала бы стража в новой + * палитре с полосой HP в старой. */ + pop_guard_set_palette(color); /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из * колонки тайла, а не берётся из файла. */ Guard.room = room; @@ -142,8 +261,13 @@ void pop_guard_enter(uint8_t room) Guard.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX); Guard.curr_col = (int8_t)(tile % 10); Guard.direction = dir; - Guard.charid = CHARID_2_GUARD; - Guard.sword = 0; /* sword_0_sheathed */ + /* seg002:196: на уровне со скелетами слот поднимает СКЕЛЕТА, и он + * встаёт сразу активным — с вынутым мечом (seq_63), а не в стойке + * покоя (seq_77). Стойку ставит ветка ниже, вместе с seq трупа. */ + Guard.charid = (pop_level_guard_type(pop_current_level) == 2) + ? CHARID_4_SKELETON : CHARID_2_GUARD; + Guard.sword = (Guard.charid == CHARID_4_SKELETON) + ? SWORD_2_DRAWN : 0; /* sword_0_sheathed */ Guard.alive = -1; Guard.action = 1; /* actions_1_run_jump */ Guard.fall_x = Guard.fall_y = 0; diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 32eecdf..9e0af53 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -33,7 +33,9 @@ #define SEQ_66_STRIKE_AFTER_PARRY 66 #define SEQ_71_DYING 71 #define SEQ_75_STRIKE 75 /* удар (Кид) */ +#define SEQ_63_GUARD_ACTIVE_AFTER_FALL 63 /* стойка АКТИВНОГО (скелет) */ #define SEQ_77_GUARD_STAND_INACTIVE 77 +#define SEQ_88_SKEL_WAKE_UP 88 /* скелет встаёт (спецсобытие) */ #define SEQ_80_STAND_FLIPPED 80 #define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */ #define SEQ_87_GUARD_BECOME_INACT 87 @@ -76,6 +78,23 @@ extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты extern uint8_t guardhp_curr, guardhp_max; extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */ +/* take_hp (seg006:06BD): отнять count HP у АКТИВНОГО персонажа (Char) — + * Киду из `hitp_*`, любому другому из `guardhp_*`. Возврат 1 = HP кончились + * (для вызывающего это «умер»); саму дельту применяет pop_do_delta_hp раз в + * кадр, как в оригинале. + * + * Живёт в резиденте W1/W2, потому что зовут ОБА банка: боёвка (guards.c, + * банк 1) и физика (pop_map.c, банк 3) — раньше у каждого была своя копия, + * причём у pop_map неверная (правила hitp_curr напрямую, мимо дельты, и не + * знала про не-Кидов). Чит бессмертия ловится здесь же. */ +uint8_t pop_take_hp(uint8_t count); + +/* Взводится в кадре, когда СТРАЖУ убавили HP (guardhp_delta < 0 оригинала): + * по нему pop_guard_draw рисует «брызги» удара, как draw_guard в seg008:1654. + * Снимает главный цикл после отрисовки — стража могло не оказаться в + * отрисованной комнате, и тогда флаг обязан просто пропасть. */ +extern uint8_t pop_guard_hurt; + /* Видит ли страж Кида (порт can_guard_see_kid, seg003:688): * 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра), * 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в @@ -109,6 +128,12 @@ extern pop_rnd_t pop_fight_seed; * check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */ void pop_check_can_guard_see_kid(void) __banked; +/* check_skel (seg002:1042) — СПЕЦСОБЫТИЕ уровня 3: скелета нет в данных + * уровня (guards_tile пуст во всех комнатах), он поднимается сам, когда Кид + * доходит до нужной колонки комнаты 1 при открытой двери уровня. Звать раз + * в кадр, ДО логики персонажей (порядок play_frame, seg000:868). */ +void pop_check_skel(void) __banked; + /* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в * кадре как в play_frame: hurting -> hurt -> do_delta_hp. */ void pop_check_sword_hurting(void) __banked; @@ -119,6 +144,11 @@ void pop_check_sword_hurt(void) __banked; extern kframe pop_gframe; void pop_guard_load_frame(void); +/* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он + * провалился ниже комнаты. Звать раз в кадр после физики и HP — как + * play_frame (seg000:884). */ +void pop_guard_fallout(void); + /* Есть ли живой страж в отрисованной комнате (для главного цикла). */ uint8_t pop_guard_present(void); @@ -135,6 +165,13 @@ void pop_guard_enter(uint8_t room); * enter_room, пока Guard ещё держит прежнего. */ void pop_guard_leave(void); +/* Страж идёт ЗА КИДОМ в соседнюю комнату — порт ветки стража из exit_room + * (seg002:0340) + follow_guard (seg002:039E). Звать ВМЕСТО пары + * leave/enter при смене комнаты: 1 = страж уже перенесён в new_room (ни + * leave, ни enter звать нельзя), 0 = остаётся в старой (всё как раньше). + * side — куда ушёл КИД: 0 left, 1 right, 2 up, 3 down. */ +uint8_t pop_guard_follow(uint8_t new_room, uint8_t side); + /* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE, * как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c * (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */ @@ -144,6 +181,12 @@ void pop_hp_draw(void) __banked; /* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */ void pop_hp_invalidate(void) __banked; +/* Цвет стража — подменой 16 слотов палитры (порт set_chtab_palette, + * seg003:257): спрайты лежат в индексах POP_GUARD_PAL_BASE + v, а цвет + * задаёт level.guards_color[room-1] (1..7; 0 = не менять). Звать ДО + * отрисовки комнаты, как это делает redraw_screen. */ +void pop_guard_set_palette(uint8_t color) __banked; + void pop_guard_heal(void) __banked; void pop_guard_draw(void) __banked; diff --git a/applications/PoP/roomtest/pop_guard_pal.h b/applications/PoP/roomtest/pop_guard_pal.h new file mode 100644 index 0000000..faeba24 --- /dev/null +++ b/applications/PoP/roomtest/pop_guard_pal.h @@ -0,0 +1,31 @@ +/* pop_guard_pal.h — палитры стражей (guard_palettes из res10.bin). + * СГЕНЕРЕНО toolchain/pop_pack_guard.py — НЕ править вручную. + * + * Индексация как в оригинале: цвет из level.guards_color[room-1] + * (младшие 4 бита), 1..7 -> строка [color-1]; 0 = палитру НЕ менять + * (seg003:255 при curr_guard_color == 0 set_chtab_palette не зовёт). + * Запись — 4 байта (B, G, R, 0), формат gfx_pal_load. */ +#ifndef POP_GUARD_PAL_H +#define POP_GUARD_PAL_H + +#include + +#define POP_GUARD_PAL_BASE 0x90 /* слоты Sprinter под chtab_5 */ +#define POP_GUARD_NPAL 7 + +static const uint8_t pop_guard_pal[POP_GUARD_NPAL][64] = { + { 0x00,0x00,0x00,0x00,0xFF,0x91,0x48,0x00,0xB2,0x81,0x69,0x00,0xFF,0xFF,0xFF,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0xD2,0xB6,0xD2,0x00,0x91,0x7D,0x91,0x00,0xFF,0xDA,0xFF,0x00,0x4C,0x81,0xA1,0x00,0x71,0xBA,0xEA,0x00,0x79,0x28,0x81,0x00,0xB2,0x81,0x69,0x00,0xC2,0xC2,0xC2,0x00,0x38,0x00,0xF6,0x00 }, + { 0x00,0x00,0x00,0x00,0x00,0x30,0xAA,0x00,0x00,0x24,0x81,0x00,0x79,0x79,0xFF,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0xC2,0x69,0x85,0x00,0xAA,0x5D,0x75,0x00,0xFF,0x89,0xB2,0x00,0x50,0x50,0x79,0x00,0x71,0x71,0xAA,0x00,0xBE,0x28,0xBA,0x00,0x00,0x24,0x81,0x00,0x61,0x61,0xCA,0x00,0x38,0x00,0xF6,0x00 }, + { 0x00,0x00,0x00,0x00,0x00,0x50,0xFF,0x00,0x00,0x38,0xBE,0x00,0xFF,0xFF,0xFF,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0x00,0x99,0xD2,0x00,0x00,0x7D,0xAA,0x00,0x00,0xBA,0xFF,0x00,0x4C,0x81,0xA1,0x00,0x71,0xBA,0xEA,0x00,0x99,0x28,0x6D,0x00,0x00,0x38,0xBE,0x00,0xD2,0xCA,0xCA,0x00,0x38,0x00,0xF6,0x00 }, + { 0x00,0x00,0x00,0x00,0x00,0x91,0x0C,0x00,0x00,0x71,0x0C,0x00,0xFF,0xD2,0xFF,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0x4C,0xAE,0xCA,0x00,0x38,0x7D,0x91,0x00,0x61,0xDA,0xFF,0x00,0x4C,0x81,0xA1,0x00,0x71,0xBA,0xEA,0x00,0x20,0x00,0x99,0x00,0x00,0x71,0x0C,0x00,0xB2,0x91,0xB2,0x00,0x38,0x00,0xF6,0x00 }, + { 0x00,0x00,0x00,0x00,0xFF,0x00,0x61,0x00,0xAA,0x00,0x61,0x00,0xB2,0x3C,0xBE,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0x8D,0xBA,0xCA,0x00,0x6D,0x8D,0x99,0x00,0xB2,0xEA,0xFF,0x00,0x34,0x6D,0x91,0x00,0x40,0x89,0xBA,0x00,0x79,0x28,0x81,0x00,0xAA,0x00,0x61,0x00,0x81,0x2C,0x89,0x00,0x38,0x00,0xF6,0x00 }, + { 0x00,0x00,0x00,0x00,0xFF,0x59,0xCA,0x00,0xC2,0x44,0x99,0x00,0xFF,0xAA,0x91,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0xD2,0xB6,0xD2,0x00,0x99,0xB2,0xB2,0x00,0xDA,0xFF,0xFF,0x00,0x4C,0x81,0xA1,0x00,0x71,0xBA,0xEA,0x00,0xA1,0x6D,0x5D,0x00,0xC2,0x44,0x99,0x00,0xD2,0x8D,0x79,0x00,0x38,0x00,0xF6,0x00 }, + { 0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0xDA,0xDE,0x00,0xE2,0x30,0xB2,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0x00,0x9D,0xD2,0x00,0x00,0x9D,0xF2,0x00,0x00,0xC2,0xFF,0x00,0xA1,0x4C,0x75,0x00,0xFF,0x79,0xBA,0x00,0xB2,0x00,0x61,0x00,0x00,0xDA,0xDE,0x00,0xAA,0x24,0x85,0x00,0x38,0x00,0xF6,0x00 }, +}; + +/* Скелет (SKEL/res750.pal): свой набор — свой цвет. */ +static const uint8_t pop_skel_pal[64] = { + 0x00,0x00,0x00,0x00,0xFF,0xFF,0x55,0x00,0x89,0x79,0xBA,0x00,0xF2,0xFF,0xFF,0x00,0x00,0x00,0xAA,0x00,0xAA,0x00,0xAA,0x00,0x00,0x55,0xAA,0x00,0xDA,0xE6,0xE6,0x00,0x55,0x55,0x55,0x00,0xFF,0x55,0x55,0x00,0x55,0xFF,0x55,0x00,0xFF,0xFF,0x55,0x00,0x55,0x55,0xFF,0x00,0x00,0x00,0x00,0x00,0x55,0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0x00, +}; + +#endif diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 10a64a8..b1928a1 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -71,7 +71,8 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, if (!sword_ok) return 0; if (!((ch->frame >= 229 && ch->frame < 238) || ch->sword != 0 || - (ch->charid == CHARID_2_GUARD && ch->alive < 0))) + ((ch->charid == CHARID_2_GUARD || ch->charid == CHARID_4_SKELETON) && + ch->alive < 0))) return 0; sf = (uint8_t)(fr->sword & 0x3F); if (sf == 0 || sf >= KID_NSWORD) return 0; @@ -88,15 +89,25 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]); top = obj_y + (int)SWORD_DY[sf] - (int)h + 1; if (flip) bx -= (int)w; - gfx_set_bank(GFX_BANK_SPRITE); - /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h: - * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */ - if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) - gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0); - else - gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0); - gfx_set_bank(GFX_BANK_NORMAL); - *rx = bx; *ry = top; *rw = w; *rh = h; + /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507): клинок + * обрезается по нему так же, как сам персонаж. Без этого у падающего + * в пропасть скелета меч оставался виден НИЖЕ поля, на полосе HP. */ + { + int skip = 0, rows = (int)h; + if (top < 0) { skip = -top; rows -= skip; top = 0; } + if (top + rows > 192) rows = 192 - top; + if (rows <= 0) return 0; + gfx_set_bank(GFX_BANK_SPRITE); + /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h: + * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */ + if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows)) + gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, + (uint8_t)skip, (uint8_t)rows); + else + gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, skip, rows); + gfx_set_bank(GFX_BANK_NORMAL); + *rx = bx; *ry = top; *rw = w; *rh = (uint16_t)rows; + } return 1; } @@ -214,34 +225,18 @@ pop_char_t Char; * ними: для стража Opp = Kid, для Кида Opp = Guard. */ pop_char_t Opp; -/* Снимок последовательности на момент входа в окно Char: по нему - * pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)» - * от «её сменил pop_map (прямо в Kid)». */ -static uint16_t kid_seq_at_load; - -void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; } +void pop_loadkid(void) { Char = Kid; } void pop_savekid(void) { Kid = Char; } -/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально - * меняет у активного персонажа — последовательность и состояние меча. +/* Выгрузка после control() — теперь ПОЛНАЯ, как savekid оригинала. * - * Почему не `Kid = Char`, как делает savekid оригинала: у нас часть - * действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq, - * safe_step...), и он пишет ПРЯМО в Kid — там ещё не портирован переход на - * Char. Полное копирование затирало эти правки: Kid терял выравнивание x - * и ряд, и спуск с уступа через вис не срабатывал — персонаж просто - * приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */ -void pop_savekid_state(void) -{ - /* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char - * (pop_char_set_seq), pop_map — прямо в Kid (kid_set_seq: спуск с - * уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше: - * диспетчер про эти переходы не знает и, копируя Char вслепую, затирал - * их — спуск в проём не срабатывал, а персонаж даже не приседал, потому - * что pop_down_action уже вернул «обработано». */ - uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load); - if (!map_changed) Kid.curr_seq = Char.curr_seq; - Kid.sword = Char.sword; -} + * Была выборочной (только curr_seq и sword) ровно потому, что pop_map правил + * геометрию ПРЯМО в Kid, мимо окна Char: полное копирование затирало его + * правки — Kid терял выравнивание x и ряд, и спуск с уступа через вис не + * срабатывал (персонаж просто приседал). Приходилось ещё и сравнивать + * последовательность со снимком на входе в окно, чтобы понять, чья правка + * свежее. С переводом физики на Char (GUARD-PHYS) диспетчер и pop_map + * пишут в одно и то же место, и разбирать это больше не нужно. */ +void pop_savekid_state(void) { Kid = Char; } void pop_loadshad(void) { Char = Guard; } void pop_saveshad(void) { Guard = Char; } void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; } @@ -323,9 +318,20 @@ static void load_frame(void) { /* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у * стража frame_tbl_guard с индексом frame + add_frame − 149, где - * add_frame = 70 для кадров 102..106 (seg006:535). */ + * add_frame = 70 для кадров 102..106 (seg006:535). + * + * СКЕЛЕТ идёт по той же ветке, что страж (seg006:529 — общий case): у + * него свой атлас, но НУМЕРАЦИЯ кадров та же. Пока здесь стоял только + * charid_2_guard, скелет попадал в таблицу Кида, получал несуществующий + * image (180 при 28 спрайтах в наборе) и не рисовался вовсе. + * ТЕНЬ (charid_1_shadow) — по таблице стража ТОЛЬКО в кадрах 150..189, + * в остальных она ходит кадрами Кида (seg006:532). */ const uint8_t *f; - if (Char.charid == CHARID_2_GUARD) { + uint8_t use_guard_tbl = + (Char.charid == CHARID_2_GUARD || Char.charid == CHARID_4_SKELETON || + (Char.charid == CHARID_1_SHADOW && + Char.frame >= 150 && Char.frame < 190)); + if (use_guard_tbl) { int16_t idx = (int16_t)Char.frame; if (idx >= 102 && idx < 107) idx += 70; idx -= 149; @@ -473,21 +479,18 @@ void play_seq(void) * check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и * зацеп». Результат менялся от кадра к кадру — вместе с анимацией стража. * - * determine_col у нас существует только для Кида: pop_map работает с Kid - * напрямую, а не с абстрактным Char. Поэтому вторую половину связки делаем - * только на его ветке — стражу пока хватает перезагрузки самого кадра (его - * колонку ведёт pop_guard_enter/leave). Char.curr_col синхронизируем - * руками: окно Char — копия Kid. */ + * determine_col теперь общий: pop_map работает с Char, поэтому колонка + * пересчитывается ЛЮБОМУ активному персонажу — ровно как load_fram_det_col + * в оригинале. Раньше вторая половина связки работала только на ветке Кида + * (стражу колонку вёл pop_guard_enter/leave) — прямое следствие того, что + * физика знала один Kid. */ void pop_load_fram_det_col(void) { if (!kdat_ok) return; gfx_w0_map(kdat_page); load_frame(); gfx_w0_unmap(); - if (Char.charid == CHARID_0_KID) { - pop_determine_col(); - Char.curr_col = Kid.curr_col; - } + pop_determine_col(); } /* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки @@ -728,7 +731,14 @@ void kid_draw(void) uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */ /* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше * линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает - * сквозь щели кладки при спуске/висе. */ + * сквозь щели кладки при спуске/висе. + * + * Окно Char обязательно: clip_char работает с АКТИВНЫМ персонажем + * (в оригинале он и зовётся внутри add_kid_to_objtable, то есть под + * loadkid), а к моменту отрисовки в Char лежит тот, кто тикал + * последним — страж. Выгружать назад нечего: clip_char только + * читает. */ + pop_loadkid(); ct = pop_clip_char_top(kid_fp_x, obj_y, w, h); if (ct > top) { skip = ct - top; diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 218d5a2..2c23264 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -84,7 +84,10 @@ #define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */ #define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */ #define SEQ_63_ACTIVE_AFTER_FALL 63 /* стойка С МЕЧОМ после приземления */ +#define SEQ_65_BUMP_FWD_SWORD 65 /* стража прижали к стене (с мечом) */ #define SEQ_81_FIGHTFALL 81 /* fightfall: падение из боевой стойки */ +#define SEQ_82_GUARD_PUSHED_OFF 82 /* стража спихнули с уступа */ +#define SEQ_83_GUARD_FALL 83 /* страж сам шагнул в провал с мечом */ #define TILE_FLOOR 1 #define FRAME_109_CROUCH 109 @@ -130,10 +133,11 @@ uint8_t hitp_max; /* стартовое HP Кида (для ин * Стартовое значение (custom->start_hitp = 3) ставит main через * pop_kid_hp_newgame — инициализатором нельзя: модуль банкованный. */ uint8_t hitp_beg_lev; -/* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки - * (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале. - * Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и - * проверен до боёвки, и переводить его на дельту без нужды рискованно. */ +/* Дельта HP Кида за кадр (порт hitp_delta). Её ставят боёвка (guards.c) и + * физика (пики/падение/плита на голову — через общий pop_take_hp), а + * применяет pop_do_delta_hp раз в кадр, как в оригинале. Раньше путь + * пик/падения правил hitp_curr напрямую; с переходом физики на Char это + * стало невозможно — урон должен уметь уходить и стражу. */ int8_t hitp_delta; static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */ @@ -144,17 +148,21 @@ static uint8_t grab_timer; /* после зацепа (12): блокиру * (порт 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). Для коллизии на швах — - * иначе Kid, войдя в соседнюю комнату у самой кромки (curr_col=-1/10), стоял - * бы «в стене» уровня и in_wall выбрасывал бы его. ВТОРАЯ колонка нужна, - * когда Kid стоит В шве и смотрит НАРУЖУ: передняя колонка тогда −2 (или 11), - * и мнимая стена там выпихивала его сквозь решётку (баг разворота у ворот). - * Оригинал резолвит любую колонку через find_room_of_tile (seg006:005D). +/* Связи комнаты (0 = нет соседа) + ЦЕЛИКОМ fg соседей слева и справа + * (g_lcol/g_rcol, раскладка комнаты [row*10 + col]). Оригинал резолвит + * любую колонку через find_room_of_tile (seg006:005D), гуляя по roomlinks; + * у нас уровень лежит в EMM-странице, маппить её на каждый get_tile (горячий + * путь физики) дорого — поэтому соседи кэшируются на входе в комнату. + * + * Почему именно ВСЯ комната, а не кромочные колонки: col считается из x + * персонажа (floor((x − 7 − 58)/14)), и на всём диапазоне x 0..255 даёт + * col от −5 до 13. Кромок не хватало, а «за кэшем» get_tile отдавал стену — + * мнимую. Так пропадала видимость стража (BUG-SWORD-GHOST-1: страж после + * follow_guard оказывался в col 12, луч упирался в мнимую стену, Кид прятал + * меч и не мог достать обратно) и так же ломались коллизия/разворот у швов. * Задаются pop_map_set_edges. */ static uint8_t g_link_l, g_link_r, g_link_u, g_link_d; -static uint8_t g_lcol[6], g_rcol[6]; +static uint8_t g_lcol[30], g_rcol[30]; /* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- * * В оригинале это тот же loose, просто get_tile ушёл в комнату сверху @@ -197,20 +205,25 @@ static void pop_kid_add_life(void) hitp_delta = (int8_t)(hitp_max - hitp_curr); } -/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */ -static uint8_t take_hp(uint8_t count) -{ - if (count >= hitp_curr) { hitp_curr = 0; return 1; } - hitp_curr = (uint8_t)(hitp_curr - count); - return 0; -} +/* Урон — через общий pop_take_hp (резидент pop_guard.c): физика работает + * над Char, значит и HP обязаны убавляться у ТОГО, кто сейчас в Char. Своя + * копия take_hp здесь была Kid-only и правила hitp_curr мимо дельты. */ -void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, - const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked +void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked { + uint8_t i; g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down; - if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; } - if (rcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_rcol[i] = rcol_fg[i]; } + /* fg соседей — прямо из уровня, одной скобкой доступа на обе комнаты. + * Держать ещё одну копию в приложении незачем: карта — единственный её + * потребитель (отрисовке швов хватает кромочных колонок pop_room_load). */ + if (left || right) { + pop_level_access_begin(); + for (i = 0; i < 30; i++) { + if (left) g_lcol[i] = pop_level_tile_raw(left, i); + if (right) g_rcol[i] = pop_level_tile_raw(right, i); + } + pop_level_access_end(); + } /* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия * недействительны (в оригинале prev_coll_room != curr_row_coll_room и * бампа на кадре входа нет). */ @@ -226,14 +239,10 @@ static uint8_t get_tile(int8_t col, int8_t row) g_curr_tile = (uint8_t)(g_above[col] & 0x1F); else if (row < 0 || row > 2) g_curr_tile = TILE_WALL; - else if (col == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */ - g_curr_tile = g_lcol[row] & 0x1F; - else if (col == -2 && g_link_l) /* на тайл глубже в комнату слева (col8) */ - g_curr_tile = g_lcol[3 + row] & 0x1F; - else if (col == 10 && g_link_r) /* правый шов: тайл col0 комнаты справа */ - g_curr_tile = g_rcol[row] & 0x1F; - else if (col == 11 && g_link_r) /* на тайл глубже в комнату справа (col1) */ - g_curr_tile = g_rcol[3 + row] & 0x1F; + else if (col < 0 && col >= -10 && g_link_l) /* уходим в комнату СЛЕВА */ + g_curr_tile = g_lcol[row * 10 + col + 10] & 0x1F; + else if (col > 9 && col <= 19 && g_link_r) /* и в комнату СПРАВА */ + g_curr_tile = g_rcol[row * 10 + col - 10] & 0x1F; else if (col < 0 || col > 9) g_curr_tile = TILE_WALL; /* край уровня / нет соседа = стена */ else @@ -267,12 +276,33 @@ uint8_t pop_tile_at(int8_t col, int8_t row) __banked static uint8_t gate_modif(int8_t col, int8_t row) { if (row < 0 || row > 2) return 0xFF; - if (col == -1 && g_link_l) return pop_trob_modif(g_link_l)[row * 10 + 9]; - if (col == 10 && g_link_r) return pop_trob_modif(g_link_r)[row * 10 + 0]; if (col >= 0 && col <= 9) return pop_trob_modif(g_room)[row * 10 + col]; + if (col < 0 && col >= -10 && g_link_l) + return pop_trob_modif(g_link_l)[row * 10 + col + 10]; + if (col > 9 && col <= 19 && g_link_r) + return pop_trob_modif(g_link_r)[row * 10 + col - 10]; return 0xFF; } +/* check_skel (seg002:1053): скелет ВСТАЁТ — его тайл стирается в пол. + * Перерисовать надо две клетки (сам тайл и соседа справа, как в оригинале: + * set_redraw_full + set_wipe на curr_tilepos и curr_tilepos+1) и на ОБЕИХ + * страницах дабл-буфера — иначе через кадр проступает старый скелет. */ +void pop_skel_wake_tile(uint8_t tilepos) __banked +{ + g_fg[tilepos] = TILE_FLOOR; + pop_set_redraw(tilepos, POP_RD_FLOOR, 2); + if ((tilepos % 10) < 9) + pop_set_redraw((uint8_t)(tilepos + 1), POP_RD_FLOOR, 2); +} + +/* Тот же модификатор наружу (луч видимости стража, guards.c): ему нужна + * openness ворот в ЛЮБОЙ колонке луча, включая ушедшие за край комнаты. */ +uint8_t pop_gate_modif(int8_t col, int8_t row) __banked +{ + return gate_modif(col, row); +} + static uint8_t gate_passable(int8_t col, int8_t row) { uint8_t gm = gate_modif(col, row); @@ -293,37 +323,37 @@ static uint8_t tile_is_floor(uint8_t t) static uint8_t get_tile_at_char(void) { - return get_tile(Kid.curr_col, Kid.curr_row); + return get_tile(Char.curr_col, Char.curr_row); } static uint8_t get_tile_infrontof_char(void) { - g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col); - return get_tile(g_infrontx, Kid.curr_row); + g_infrontx = (int8_t)(dir_front[Char.direction + 1] + Char.curr_col); + return get_tile(g_infrontx, Char.curr_row); } /* ---- Тайлы НАД/ЗА персонажем (для зацепа/спуска, seg006) ----------- */ static uint8_t get_tile_above_char(void) { - return get_tile(Kid.curr_col, (int8_t)(Kid.curr_row - 1)); + return get_tile(Char.curr_col, (int8_t)(Char.curr_row - 1)); } static uint8_t get_tile_behind_char(void) { - return get_tile((int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col), - Kid.curr_row); + return get_tile((int8_t)(dir_behind[Char.direction + 1] + Char.curr_col), + Char.curr_row); } static uint8_t get_tile_front_above_char(void) { - g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col); - return get_tile(g_infrontx, (int8_t)(Kid.curr_row - 1)); + g_infrontx = (int8_t)(dir_front[Char.direction + 1] + Char.curr_col); + return get_tile(g_infrontx, (int8_t)(Char.curr_row - 1)); } static uint8_t get_tile_behind_above_char(void) { - return get_tile((int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col), - (int8_t)(Kid.curr_row - 1)); + return get_tile((int8_t)(dir_behind[Char.direction + 1] + Char.curr_col), + (int8_t)(Char.curr_row - 1)); } static uint8_t wall_type(uint8_t t) @@ -341,8 +371,8 @@ static uint8_t wall_type(uint8_t t) static int char_dx_forward(int8_t dx) { int d = dx; - if (Kid.direction < 0) d = -d; - return (int)Kid.x + d; + if (Char.direction < 0) d = -d; + return (int)Char.x + d; } /* col = (xpos-58)/14 округл. вниз; g_obj_xl = позиция в тайле (0..13). */ @@ -367,7 +397,7 @@ static int dx_weight(void) static void determine_col(void) { - Kid.curr_col = get_tile_div_mod_m7(dx_weight()); + Char.curr_col = get_tile_div_mod_m7(dx_weight()); } /* determine_col наружу — для pop_load_fram_det_col (pop_kid): порт связки @@ -381,7 +411,7 @@ static int distance_to_edge(int xpos) int d; get_tile_div_mod_m7(xpos); /* -> g_obj_xl */ d = g_obj_xl; - if (Kid.direction == 0) d = TILE_RIGHTX - d; /* dir_0_right */ + if (Char.direction == 0) d = TILE_RIGHTX - d; /* dir_0_right */ return d; } static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); } @@ -404,7 +434,7 @@ static int char_x_forward_edge(void) { int e = char_dx_forward(kid_cur_dx()); uint8_t fl = kid_cur_flags(); - if (Kid.direction < 0) fl ^= 0x80; + if (Char.direction < 0) fl ^= 0x80; if (!(fl & 0x80) && e <= 57) e++; /* odd pixel + C-трунк к нулю */ return e; } @@ -413,7 +443,7 @@ static int char_front_coll(void) { int cx = char_x_forward_edge(); if (kid_cur_flags() & FRAME_THIN) - cx += (Kid.direction < 0) ? 4 : -4; /* THIN: край внутрь */ + cx += (Char.direction < 0) ? 4 : -4; /* THIN: край внутрь */ return cx; } @@ -424,7 +454,7 @@ static int dist_from_wall_forward(uint8_t tiletype, int8_t tcol) if (type == 0) return 127; coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; cx = char_front_coll(); - if (Kid.direction < 0) /* лицом влево */ + if (Char.direction < 0) /* лицом влево */ return cx - (coll_left + TILE_RIGHTX - wall_dr[type]); else /* лицом вправо */ return wall_dl[type] + coll_left - cx; @@ -442,13 +472,13 @@ static void in_wall(void) d = 6 - d; else d += 4; - Kid.x = (uint8_t)char_dx_forward((int8_t)d); + Char.x = (uint8_t)char_dx_forward((int8_t)d); determine_col(); get_tile_at_char(); } /* ---- Падение ------------------------------------------------------- */ -static void inc_curr_row(void) { Kid.curr_row++; } +static void inc_curr_row(void) { Char.curr_row++; } /* Пики (определены ниже, перед pop_phys_tick) — нужны в land() для смерти * при ПАДЕНИИ на выдвинутые пики (seg005 loc_5EE6). */ @@ -462,12 +492,12 @@ static uint8_t fell_on_spikes(void) { uint8_t tp; if (get_tile_at_char() == TILE_SPIKE) { - tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); + tp = (uint8_t)(Char.curr_row * 10 + Char.curr_col); } else if (distance_to_edge_weight() >= 12 && get_tile_behind_char() == TILE_SPIKE) { - int8_t bcol = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col); + int8_t bcol = (int8_t)(dir_behind[Char.direction + 1] + Char.curr_col); if (bcol < 0 || bcol > 9) return 0; - tp = (uint8_t)(Kid.curr_row * 10 + bcol); + tp = (uint8_t)(Char.curr_row * 10 + bcol); } else { return 0; } @@ -478,60 +508,64 @@ static uint8_t fell_on_spikes(void) static void land(void) { uint8_t seq; - Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; if (fell_on_spikes()) return; /* упал на вредные пики — смерть */ /* к краю пола — чуть назад (как оригинал) */ if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) - Kid.x = (uint8_t)char_dx_forward(-3); - /* Урон от падения (seg005 land): 1 этаж — мягко; 2 этажа — −1 HP и - * присед; 3+ — насмерть. pop_kid_hurt = hitp_delta<0 оригинала: по нему - * приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt, - * seg003:0AFC). */ - if (Kid.fall_y < 22) { - /* seg005:176 — ВЕТКА С МЕЧОМ, и она не косметика. seq_17_soft_land - * заканчивается ВЕЧНЫМ циклом на кадре 109 (`jmp softland_crouch` в - * seqtbl), из которого выводит только control_crouched(). А - * control() при `Char.sword == sword_2_drawn` до него не доходит: - * управление уходит в control_with_sword, который знает лишь кадр - * 171 (стойка с мечом). То есть Кид, упавший на этаж С ВЫНУТЫМ - * МЕЧОМ, в этот присед садится НАВСЕГДА — ровно то, что поймали на - * приёмке уровня 2 (BUG-LAND-SWORD-1). Оригинал такой позы не - * допускает: с мечом приземление даёт боевую стойку. */ - if (Kid.sword == SWORD_2_DRAWN) { + Char.x = (uint8_t)char_dx_forward(-3); + /* Урон от падения (seg005:173) — ветвление по ПЕРСОНАЖУ, а не только по + * высоте. Порт построчно: + * 1 этаж (fall_y < 22): не-Кид (charid >= guard) ИЛИ меч вынут → + * меч в руку + seq_63; иначе присед seq_17. Звук/«нашумел» — только + * у Кида (у стража своё падение никого не будит). + * 2 этажа (< 33): тень падает как с одного этажа, СТРАЖ гибнет сразу, + * Кид (и скелет — баг оригинала, сохраняем) теряет 1 HP; + * 3+ этажа: гибнут все. + * + * Ветка «с мечом» на одном этаже — не косметика: seq_17_soft_land + * заканчивается ВЕЧНЫМ циклом на кадре 109, из которого выводит только + * control_crouched(), а при `sword == sword_2_drawn` управление уходит в + * control_with_sword и до него не доходит — Кид садится в присед + * НАВСЕГДА (BUG-LAND-SWORD-1). */ + if (Char.fall_y < 22) { + soft_land: + if (Char.charid >= CHARID_2_GUARD || Char.sword == SWORD_2_DRAWN) { + Char.sword = SWORD_2_DRAWN; seq = SEQ_63_ACTIVE_AFTER_FALL; } else { seq = SEQ_17_SOFT_LAND; } /* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */ - is_guard_notice = 1; + if (Char.charid == CHARID_0_KID) is_guard_notice = 1; } else { - uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */ - pop_kid_hurt = 1; - Kid.fall_x = Kid.fall_y = 0; - if (deadly || take_hp(1)) { /* 3+ этажа или последнее HP */ - take_hp(100); - kid_set_seq(SEQ_22_CRUSHED); - pop_kid_play(); + uint8_t deadly = (uint8_t)(Char.fall_y >= 33); /* СНЯТЬ до обнуления */ + if (!deadly && Char.charid == CHARID_1_SHADOW) goto soft_land; /* seg005:189 */ + if (!deadly && Char.charid == CHARID_2_GUARD) deadly = 1; /* seg005:190 */ + Char.fall_x = Char.fall_y = 0; + if (deadly || pop_take_hp(1)) { /* 3+ этажа или последнее HP */ + pop_take_hp(100); + pop_char_set_seq(SEQ_22_CRUSHED); + play_seq(); determine_col(); - pop_kid_dead = 1; + if (Char.charid == CHARID_0_KID) pop_kid_dead = 1; return; } - is_guard_notice = 1; /* seg005:195 */ + if (Char.charid == CHARID_0_KID) is_guard_notice = 1; /* seg005:195 */ seq = SEQ_20_MEDIUM_LAND; } - Kid.fall_x = Kid.fall_y = 0; - kid_set_seq(seq); - pop_kid_play(); + Char.fall_x = Char.fall_y = 0; + pop_char_set_seq(seq); + play_seq(); determine_col(); } static void start_fall(void) { - uint8_t frame = Kid.frame, seq, tile; + uint8_t frame = Char.frame, seq, tile; /* seg006:1044 первым делом убирает меч в ножны: дальше падением рулит * не боевой диспетчер, а обычный. Без этого Kid летит «с клинком», и * control_with_sword разбирает кадры падения как боевые. */ - Kid.sword = SWORD_0_SHEATHED; + Char.sword = SWORD_0_SHEATHED; inc_curr_row(); /* start_chompers() — чомперов ещё нет (L3-CHOMP). */ /* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */ @@ -541,24 +575,36 @@ static void start_fall(void) else if (frame >= 81 && frame < 86) { /* сорвался, приземляясь после прыжка вверх: сдвиг ВПЕРЁД на 5 */ seq = SEQ_19_FALL; - Kid.x = (uint8_t)char_dx_forward(5); + Char.x = (uint8_t)char_dx_forward(5); pop_load_fram_det_col(); } else if (frame >= 150 && frame < 180) { - /* Кадры С МЕЧОМ (150..179) — отступил в провал. У оригинала это - * ОТДЕЛЬНАЯ последовательность seq_81 (fightfall): падение строго - * вниз (set_fall(0,15)), а не seq_7 с дрейфом fall_x=1 — из-за - * дрейфа наш Kid уезжал на ~тайл в сторону за время падения. - * Плюс сдвиг на 5 назад, если стоит у самой кромки лицом влево. - * Ветка стража (seq_82/83) не нужна: start_fall — только Kid. - * TODO: droppedout=1 (guard_follows_kid_down, guards.c:296). */ - if (Kid.direction < 0 && distance_to_edge_weight() <= 7) - Kid.x = (uint8_t)char_dx_forward(-5); - seq = SEQ_81_FIGHTFALL; + /* Кадры С МЕЧОМ (150..179) — ушёл в провал из боевой стойки + * (seg006:1123). Отдельная последовательность, а не seq_7: у той + * дрейф fall_x=1, и персонаж уезжал на ~тайл в сторону за полёт. + * Ветвление по персонажу — как в оригинале: + * СТРАЖ: спихнули (fall_x < 0) → seq_82, сам шагнул → seq_83; + * ПРОЧИЕ (Кид): seq_81 + взвести droppedout — по нему страж + * решает, идти ли следом вниз (guard_follows_kid_down). */ + if (Char.charid == CHARID_2_GUARD) { + if (Char.fall_x < 0) { + if (Char.direction < 0 && distance_to_edge_weight() <= 7) + Char.x = (uint8_t)char_dx_forward(-5); + seq = SEQ_82_GUARD_PUSHED_OFF; + } else { + pop_droppedout = 0; + seq = SEQ_83_GUARD_FALL; + } + } else { + pop_droppedout = 1; + if (Char.direction < 0 && distance_to_edge_weight() <= 7) + Char.x = (uint8_t)char_dx_forward(-5); + seq = SEQ_81_FIGHTFALL; + } } else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */ - kid_set_seq(seq); - pop_kid_play(); + pop_char_set_seq(seq); + play_seq(); determine_col(); /* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2). * Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose) @@ -566,7 +612,7 @@ static void start_fall(void) * КОМНАТА СНИЗУ (у дыры — пол/пусто, не стена). Без гарда in_wall() ложно * срабатывал и откатывал X назад (6−d) → дайв падения смещался на тайл * влево (Кид «клевал» рано над колонной вместо дыры). */ - if (Kid.curr_row <= 2) { + if (Char.curr_row <= 2) { if (get_tile_at_char() == TILE_WALL) { in_wall(); return; @@ -577,7 +623,7 @@ static void start_fall(void) tile = get_tile_infrontof_char(); if (tile == TILE_WALL) { if (frame != 44 || distance_to_edge_weight() >= 6) - Kid.x = (uint8_t)char_dx_forward(-1); + Char.x = (uint8_t)char_dx_forward(-1); determine_col(); } } @@ -589,9 +635,9 @@ static void start_fall(void) static uint8_t can_grab(uint8_t through) { if (through == TILE_WALL) return 0; /* сквозь стену нельзя */ - if (through == TILE_DOORTOP && Kid.direction >= 0) return 0; /* верх двери, лицом вправо */ + if (through == TILE_DOORTOP && Char.direction >= 0) return 0; /* верх двери, лицом вправо */ if (tile_is_floor(through)) return 0; /* сквозь пол нельзя */ - if (g_curr_tile == TILE_DOORTOP_FLOOR && Kid.direction < 0) return 0; + if (g_curr_tile == TILE_DOORTOP_FLOOR && Char.direction < 0) return 0; if (!tile_is_floor(g_curr_tile)) return 0; /* не за что цепляться */ return 1; } @@ -611,20 +657,20 @@ static void check_grab(void) { uint8_t old_x; if (!pop_ctrl_shift_held()) return; /* Shift не зажат */ - if ((uint8_t)Kid.fall_y >= 32) return; /* падает слишком быстро */ - if ((uint16_t)pop_y_land[Kid.curr_row + 1] > (uint16_t)(Kid.y + 25)) return; - old_x = Kid.x; - Kid.x = (uint8_t)char_dx_forward(-8); + if ((uint8_t)Char.fall_y >= 32) return; /* падает слишком быстро */ + if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return; + old_x = Char.x; + Char.x = (uint8_t)char_dx_forward(-8); determine_col(); if (!can_grab_front_above()) { - Kid.x = old_x; /* не за что — назад */ + Char.x = old_x; /* не за что — назад */ determine_col(); } else { - Kid.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight()); - Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; - Kid.fall_y = 0; - kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR); - pop_kid_play(); + Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight()); + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + Char.fall_y = 0; + pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR); + play_seq(); determine_col(); grab_timer = 12; /* (TODO: sound grab) */ } @@ -656,26 +702,26 @@ static void make_loose_fall(int pos, uint8_t modifier); * pop_trob_modif/g_fg у нас про свою комнату; такие случаи пропускаем. */ static uint8_t check_grab_run_jump(void) { - uint8_t frame = Kid.frame; + uint8_t frame = Char.frame; uint8_t grab_tile; int8_t grab_col, grab_row; /* Сам выключатель проверяют ТОЧКИ ВЫЗОВА (иначе SDCC ругается на * недостижимый код при POP_ENABLE_JUMP_GRAB = 1). */ - if (Kid.action != ACT_RUNJUMP) return 0; + if (Char.action != ACT_RUNJUMP) return 0; if (!((frame >= 22 && frame <= 23) || (frame >= 39 && frame <= 41))) return 0; if (control_x != POP_CONTROL_RELEASED || control_y != POP_CONTROL_HELD_UP) return 0; if (!can_grab_front_above()) return 0; grab_tile = g_curr_tile; /* тайл, за который взялись */ grab_col = g_infrontx; - grab_row = (int8_t)(Kid.curr_row - 1); /* СНЯТЬ до play_seq: он двигает ряд */ + grab_row = (int8_t)(Char.curr_row - 1); /* СНЯТЬ до play_seq: он двигает ряд */ - Kid.x = (uint8_t)(pop_x_bump[grab_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX); - Kid.x = (uint8_t)char_dx_forward(Kid.direction < 0 ? -12 : 2); - Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; - kid_set_seq(SEQ_9_GRAB_WHILE_JUMPING); - pop_kid_play(); + Char.x = (uint8_t)(pop_x_bump[grab_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX); + Char.x = (uint8_t)char_dx_forward(Char.direction < 0 ? -12 : 2); + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + pop_char_set_seq(SEQ_9_GRAB_WHILE_JUMPING); + play_seq(); determine_col(); grab_timer = 12; @@ -696,16 +742,16 @@ static uint8_t check_grab_run_jump(void) static void do_fall(void) { - uint8_t nrow = (uint8_t)(Kid.curr_row + 1); + uint8_t nrow = (uint8_t)(Char.curr_row + 1); if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */ - if ((uint16_t)pop_y_land[nrow] > (uint16_t)Kid.y) { + if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) { check_grab(); /* ещё летит — попытка зацепа */ } else { if (get_tile_at_char() == TILE_WALL) in_wall(); if (tile_is_floor(g_curr_tile)) land(); - else if (Kid.curr_row < 2) + else if (Char.curr_row < 2) inc_curr_row(); /* следующий ряд ВНУТРИ комнаты */ /* curr_row==2, нет пола: продолжает падать (fall_speed двигает y); * пересечение нижней границы комнаты ловит check_leave_below (y>=211, @@ -715,18 +761,18 @@ static void do_fall(void) static void fall_accel(void) { - if (Kid.action == ACT_FREEFALL) { - int fy = (uint8_t)Kid.fall_y + FALL_ACCEL; + if (Char.action == ACT_FREEFALL) { + int fy = (uint8_t)Char.fall_y + FALL_ACCEL; if (fy > FALL_MAX) fy = FALL_MAX; - Kid.fall_y = (int8_t)fy; + Char.fall_y = (int8_t)fy; } } static void fall_speed(void) { - Kid.y = (uint8_t)(Kid.y + (uint8_t)Kid.fall_y); - if (Kid.action == ACT_FREEFALL) { - Kid.x = (uint8_t)char_dx_forward(Kid.fall_x); + Char.y = (uint8_t)(Char.y + (uint8_t)Char.fall_y); + if (Char.action == ACT_FREEFALL) { + Char.x = (uint8_t)char_dx_forward(Char.fall_x); determine_col(); } } @@ -751,13 +797,13 @@ int pop_edge_distance(void) __banked determine_col(); t = get_tile_at_char(); if (wall_type(t)) { - d = dist_from_wall_forward(t, Kid.curr_col); + d = dist_from_wall_forward(t, Char.curr_col); if (d >= 0) return edge_classify(d); } t = get_tile_infrontof_char(); /* Верх двери лицом ВПРАВО — кромка (в оригинале ПЕРВОЙ проверкой, до * wall_type: у doortop wall_type != 0). */ - if (t == TILE_DOORTOP && Kid.direction >= 0) { + if (t == TILE_DOORTOP && Char.direction >= 0) { g_edge_type = EDGE_CLOSER; return distance_to_edge_weight(); } @@ -770,7 +816,7 @@ int pop_edge_distance(void) __banked * Kid ровно до границы плиты (distance -> 0), там safe_step играет * seq_44 (testfoot) — Kid щупает плиту ногой, та трясётся (SEQ_KNOCK_DOWN), * и он остаётся на месте; шаг НА плиту — только со второго нажатия - * (Kid.repeat). Без этой ветки плита читалась как обычный пол, и Kid + * (Char.repeat). Без этой ветки плита читалась как обычный пол, и Kid * заходил на неё первым же осторожным шагом и падал вместе с ней. */ if (t == TILE_LOOSE) { g_edge_type = EDGE_CLOSER; @@ -792,7 +838,7 @@ uint8_t pop_edge_type(void) __banked { return g_edge_type; } /* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- * * pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в * пустоту / seq_14 в потолок) ЛИБО прыжок-и-зацеп за уступ выше (seq_8/ - * 24 — с полом сзади, seq_16 — без), и выравнивает Kid.x. control_up + * 24 — с полом сзади, seq_16 — без), и выравнивает Char.x. control_up * гасит caller (pop_ctrl). Порт: check_jump_up/grab_up_with_floor_behind/ * jump_up_or_grab/grab_up_no_floor_behind/jump_up (без super-high-jump). */ static uint8_t grab_up_with_floor_behind(void) @@ -800,10 +846,10 @@ static uint8_t grab_up_with_floor_behind(void) int distance = distance_to_edge_weight(); int edge_distance = pop_edge_distance(); /* побочно ставит g_edge_type */ if (distance < 4 && edge_distance < 4 && g_edge_type != EDGE_WALL) { - Kid.x = (uint8_t)char_dx_forward((int8_t)distance); + Char.x = (uint8_t)char_dx_forward((int8_t)distance); return SEQ_8_GRAB_STRAIGHT; } - Kid.x = (uint8_t)char_dx_forward((int8_t)(distance - 4)); + Char.x = (uint8_t)char_dx_forward((int8_t)(distance - 4)); return SEQ_24_GRAB_FORWARD; } @@ -813,9 +859,9 @@ static uint8_t jump_up_plain(void) /* seg005 jump_up (без sup int8_t col; uint8_t t; if (distance < 4 && g_edge_type == EDGE_WALL) - Kid.x = (uint8_t)char_dx_forward((int8_t)(distance - 3)); + Char.x = (uint8_t)char_dx_forward((int8_t)(distance - 3)); col = get_tile_div_mod(dx_weight() - 6); - t = get_tile(col, (int8_t)(Kid.curr_row - 1)); + t = get_tile(col, (int8_t)(Char.curr_row - 1)); if (t != TILE_WALL && !tile_is_floor(t)) return SEQ_28_JUMP_NOTHING_ABOVE; return SEQ_14_JUMP_INTO_CEILING; @@ -824,7 +870,7 @@ static uint8_t jump_up_plain(void) /* seg005 jump_up (без sup static uint8_t grab_up_no_floor_behind(void) { get_tile_above_char(); - Kid.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 10)); + Char.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 10)); return SEQ_16_JUMP_UP_AND_GRAB; } @@ -835,7 +881,7 @@ static uint8_t jump_up_or_grab(void) return jump_up_plain(); if (!tile_is_floor(get_tile_behind_char())) return grab_up_no_floor_behind(); - Kid.x = (uint8_t)char_dx_forward((int8_t)(distance - TILE_SIZEX)); + Char.x = (uint8_t)char_dx_forward((int8_t)(distance - TILE_SIZEX)); determine_col(); /* load_fram_det_col */ return grab_up_with_floor_behind(); } @@ -863,7 +909,7 @@ uint8_t pop_jump_up_seq(void) __banked * тайла (перелёт с колонки 5 на колонку 1) становится непроходимым — * BUG-RJUMP-1, уровень 2, комнаты 1 и 9. * - * Возврат: 1 — прыгать (Kid.x уже подогнан), 0 — прыжок ОТМЕНИТЬ. Отмена + * Возврат: 1 — прыгать (Char.x уже подогнан), 0 — прыжок ОТМЕНИТЬ. Отмена * в оригинале НЕ гасит control_up: Кид бежит дальше с зажатой «вверх», и на * следующем кадре попытка повторяется — так игрок ловит нужную фазу, просто * удерживая клавишу. */ @@ -875,8 +921,8 @@ uint8_t pop_run_jump_align(void) __banked for (tf = 0; tf < 2; tf++) { uint8_t t; - col = (int8_t)(col + dir_front[Kid.direction + 1]); - t = get_tile(col, Kid.curr_row); + col = (int8_t)(col + dir_front[Char.direction + 1]); + t = get_tile(col, Char.curr_row); if (t != TILE_SPIKE && tile_is_floor(t)) continue; /* тут пол — дальше */ { @@ -887,7 +933,7 @@ uint8_t pop_run_jump_align(void) __banked * Ветка с adj = -3 недостижима: distance_to_edge ∈ [0,13] и * tf ∈ {0,1} дают adj ∈ [-14,13], а туда нужно adj >= 128. */ if (adj < -8 || adj > -1) return 0; - Kid.x = (uint8_t)char_dx_forward((int8_t)(adj + 4)); + Char.x = (uint8_t)char_dx_forward((int8_t)(adj + 4)); } break; } @@ -904,7 +950,7 @@ int pop_wall_ahead(void) __banked /* ---- Вис (control_hanging из seg005) — тайловые решения ------------ * * Управление держится в pop_ctrl (control_*), сюда вынесены только - * тайловые предикаты + выбор seq (+ выравнивание Kid.x). */ + * тайловые предикаты + выбор seq (+ выравнивание Char.x). */ /* grab_timer истёк — можно подтягиваться? */ uint8_t pop_hang_grab_ready(void) __banked { return grab_timer == 0; } @@ -918,9 +964,9 @@ uint8_t pop_hang_grab_ready(void) __banked { return grab_timer == 0; } uint8_t pop_hang_climb_up_seq(void) __banked { uint8_t t = get_tile_above_char(); - if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) || - (t == TILE_GATE && Kid.direction != 0 && - (uint8_t)(gate_modif(Kid.curr_col, (int8_t)(Kid.curr_row - 1)) >> 2) < 6)) + if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Char.direction == 0) || + (t == TILE_GATE && Char.direction != 0 && + (uint8_t)(gate_modif(Char.curr_col, (int8_t)(Char.curr_row - 1)) >> 2) < 6)) return SEQ_73_CLIMB_UP_TO_GATE; return SEQ_10_CLIMB_UP; } @@ -930,9 +976,9 @@ uint8_t pop_hang_climb_up_seq(void) __banked int8_t pop_hang_wall(void) __banked { uint8_t t = get_tile_at_char(); - if (Kid.action != ACT_HANGSTRAIGHT && + if (Char.action != ACT_HANGSTRAIGHT && (t == TILE_WALL || - (Kid.direction < 0 && (t == TILE_DOORTOP_FLOOR || t == TILE_DOORTOP)))) + (Char.direction < 0 && (t == TILE_DOORTOP_FLOOR || t == TILE_DOORTOP)))) return 1; if (!tile_is_floor(get_tile_above_char())) return -1; @@ -941,7 +987,7 @@ int8_t pop_hang_wall(void) __banked /* hang_fall (seg005:0A46): сорваться. Нет пола ни сзади, ни под * персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7) - * и seq_11 (встать, конец спуска). Меняет Kid.x, возвращает seq. */ + * и seq_11 (встать, конец спуска). Меняет Char.x, возвращает seq. */ uint8_t pop_hang_fall_seq(void) __banked { uint8_t behind = get_tile_behind_char(); @@ -950,14 +996,14 @@ uint8_t pop_hang_fall_seq(void) __banked return SEQ_23_RELEASE_LEDGE_FALL; at = get_tile_at_char(); if (at == TILE_WALL || - (Kid.direction < 0 && (at == TILE_DOORTOP_FLOOR || at == TILE_DOORTOP))) - Kid.x = (uint8_t)char_dx_forward(-7); + (Char.direction < 0 && (at == TILE_DOORTOP_FLOOR || at == TILE_DOORTOP))) + Char.x = (uint8_t)char_dx_forward(-7); return SEQ_11_RELEASE_LEDGE_LAND; } /* down_pressed (seg005:04C7) tile-часть: 1 = обработано (НЕ приседать — * либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 = - * приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки + * приседать. Меняет Char.x. Kid развёрнут от края (край сзади) у кромки * пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */ /* Ветка «войти в дверь уровня» из up_pressed (seg005:0482) вместе с * go_up_leveldoor (seg005:0574). Дверь рядом и открыта → поставить Кида в @@ -988,15 +1034,15 @@ uint8_t pop_leveldoor_enter(void) __banked if (pop_level_start_room() == g_room) return 0; - cols[0] = Kid.curr_col; - cols[1] = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col); - cols[2] = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col); + cols[0] = Char.curr_col; + cols[1] = (int8_t)(dir_behind[Char.direction + 1] + Char.curr_col); + cols[2] = (int8_t)(dir_front[Char.direction + 1] + Char.curr_col); for (i = 0; i < 3; i++) { - if (get_tile(cols[i], Kid.curr_row) != TILE_LEVELDOOR_LEFT) + if (get_tile(cols[i], Char.curr_row) != TILE_LEVELDOOR_LEFT) continue; /* дверь найдена — дальше решает только её modif (см. выше) */ if (cols[i] < 0 || cols[i] > 9) return 0; /* дверь в шве: не наша */ - if (pop_trob_modif(g_room)[Kid.curr_row * 10 + cols[i]] < 42) + if (pop_trob_modif(g_room)[Char.curr_row * 10 + cols[i]] < 42) return 0; /* go_up_leveldoor (seg005:0574): встать РОВНО в проём и развернуться * ВЛЕВО. Направление тут не косметика: все DX последовательности @@ -1004,8 +1050,8 @@ uint8_t pop_leveldoor_enter(void) __banked * char_dx_forward при взгляде влево меняет им знак — то есть именно * dir_FF_left и гонит персонажа ВПРАВО-ВВЕРХ по лестнице. При * взгляде вправо тот же seq уводит его влево, ИЗ проёма. */ - Kid.x = (uint8_t)(pop_x_bump[cols[i] + FIRST_ONSCREEN_COLUMN] + 10); - Kid.direction = -1; + Char.x = (uint8_t)(pop_x_bump[cols[i] + FIRST_ONSCREEN_COLUMN] + 10); + Char.direction = -1; return 1; } return 0; @@ -1014,7 +1060,7 @@ uint8_t pop_leveldoor_enter(void) __banked uint8_t pop_down_action(void) __banked { if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) { - Kid.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */ + Char.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */ determine_col(); return 1; } @@ -1024,10 +1070,10 @@ uint8_t pop_down_action(void) __banked /* seg005:482: спуск с тайла-ворот лицом ВЛЕВО запрещён только пока они * ЗАКРЫТЫ (modif >> 2 < 6); под поднятой решёткой спуск обычный. */ if (can_grab(through) && - (Kid.direction >= 0 || get_tile_at_char() != TILE_GATE || - (uint8_t)(gate_modif(Kid.curr_col, Kid.curr_row) >> 2) >= 6)) { - Kid.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 9)); - kid_set_seq(SEQ_68_CLIMB_DOWN); + (Char.direction >= 0 || get_tile_at_char() != TILE_GATE || + (uint8_t)(gate_modif(Char.curr_col, Char.curr_row) >> 2) >= 6)) { + Char.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 9)); + pop_char_set_seq(SEQ_68_CLIMB_DOWN); return 1; } } @@ -1116,24 +1162,24 @@ uint8_t pop_get_item_action(void) __banked t = get_tile_at_char(); if (t == TILE_POTION || t == TILE_SWORD) { /* стоим НА предмете */ if (!tile_is_floor(get_tile_behind_char())) return 0; - Kid.x = (uint8_t)char_dx_forward(-TILE_SIZEX); /* отступить на тайл */ + Char.x = (uint8_t)char_dx_forward(-TILE_SIZEX); /* отступить на тайл */ determine_col(); } t = get_tile_infrontof_char(); /* заодно ставит g_infrontx */ if (t != TILE_POTION && t != TILE_SWORD) return 0; icol = g_infrontx; if (icol < 0 || icol > 9) return 0; /* предмет за швом — не наш */ - if (Kid.frame != FRAME_109_CROUCH) { + if (Char.frame != FRAME_109_CROUCH) { /* Ещё стоим: подровняться к предмету и присесть. */ int d = pop_edge_distance(); /* ставит g_edge_type */ if (g_edge_type != EDGE_FLOOR) - Kid.x = (uint8_t)char_dx_forward((int8_t)d); - if (Kid.direction >= 0) - Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2)); + Char.x = (uint8_t)char_dx_forward((int8_t)d); + if (Char.direction >= 0) + Char.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2)); return 1; } { - uint8_t tp = (uint8_t)(Kid.curr_row * 10 + icol); + uint8_t tp = (uint8_t)(Char.curr_row * 10 + icol); if (t == TILE_SWORD) { do_pickup(-1, tp); return 2; /* поднять меч (seq_91) */ @@ -1158,7 +1204,7 @@ static void check_on_floor(void) static void check_action(void) { - uint8_t action = Kid.action, frame = Kid.frame; + uint8_t action = Char.action, frame = Char.frame; /* Зацеп прямо в прыжке (улучшение SDLPoP, seg006:0914) — ДО всего * остального: если получилось, кадр уже сменён на вис. */ if (POP_ENABLE_JUMP_GRAB && action == ACT_RUNJUMP && @@ -1193,12 +1239,12 @@ static void check_action(void) * не меняем, только гасим горизонтальную составляющую. */ static void bumped_fall(void) { - Kid.x = (uint8_t)char_dx_forward(-4); - if (Kid.action == ACT_FREEFALL) { - Kid.fall_x = 0; + Char.x = (uint8_t)char_dx_forward(-4); + if (Char.action == ACT_FREEFALL) { + Char.fall_x = 0; } else { - kid_set_seq(SEQ_45_BUMPFALL); - pop_kid_play(); + pop_char_set_seq(SEQ_45_BUMPFALL); + play_seq(); } /* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о * стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел» @@ -1217,19 +1263,19 @@ static void bumped_floor(void) uint8_t frame; /* Оригинал сравнивает БЕЗЗНАКОВО: если персонаж НИЖЕ уровня пола, * разность заворачивается в большое число и тоже даёт «падать». */ - if ((uint16_t)(pop_y_land[Kid.curr_row + 1] - (int16_t)Kid.y) >= 15) { + if ((uint16_t)(pop_y_land[Char.curr_row + 1] - (int16_t)Char.y) >= 15) { bumped_fall(); /* высоко над полом — в воздухе */ return; } - Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; /* ПРИЖАТЬ к полу */ - if (Kid.fall_y >= 22) { /* влетел быстро — только отжать */ - Kid.x = (uint8_t)char_dx_forward(-5); + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; /* ПРИЖАТЬ к полу */ + if (Char.fall_y >= 22) { /* влетел быстро — только отжать */ + Char.x = (uint8_t)char_dx_forward(-5); return; } - Kid.fall_y = 0; + Char.fall_y = 0; /* `if (Char.alive)` (seg004:0520) — у ЖИВОГО (alive = −1) сиквенс удара * ставится ВСЕГДА, даже если персонаж уже в отскоке. Раньше здесь стоял - * наш guard `if (Kid.action == ACT_BUMPED) return;` («не рестартить») — и + * наш guard `if (Char.action == ACT_BUMPED) return;` («не рестартить») — и * он был корнем BUG-STANDUP-1: * 1) вставание из приседа (medland) двигает Кида на 1 px в стену — * срабатывает бамп; @@ -1243,16 +1289,16 @@ static void bumped_floor(void) * в комнату снизу. * У оригинала шага (3) нет: seq_47 обрывает medland, и лишних dy не * случается. */ - if (!Kid.alive) return; /* труп — только выравнивание */ - frame = Kid.frame; + if (!Char.alive) return; /* труп — только выравнивание */ + frame = Char.frame; /* Кадры прыжков и начала падения — «жёсткий» удар с приседанием * (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */ if (frame == 24 || frame == 25 || (frame >= 40 && frame < 43) || (frame >= 102 && frame < 107)) - kid_set_seq(SEQ_46_HARDBUMP); + pop_char_set_seq(SEQ_46_HARDBUMP); else - kid_set_seq(SEQ_47_BUMP); - pop_kid_play(); + pop_char_set_seq(SEQ_47_BUMP); + play_seq(); is_guard_notice = 1; /* bumped_sound (seg004:05F1) */ } @@ -1323,7 +1369,7 @@ 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; } + if (Char.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; } } @@ -1379,20 +1425,20 @@ static void check_collisions(void) int8_t col; bump_col_l = bump_col_r = BUMP_NONE; /* seg004:0044 — на кадрах разворота коллизии не считаются вовсе. */ - if (Kid.action == ACT_TURN) return; + if (Char.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; } + if (Char.curr_row < 0 || Char.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; } set_char_collision(); - move_coll_to_prev(Kid.curr_row); - coll_prev_row = Kid.curr_row; + move_coll_to_prev(Char.curr_row); + coll_prev_row = Char.curr_row; /* Порядок важен: prev уже забран, теперь три ряда пересчитываются * НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). */ - get_row_collision_data(Kid.curr_row, coll_curr); - get_row_collision_data((int8_t)(Kid.curr_row + 1), coll_below); - get_row_collision_data((int8_t)(Kid.curr_row - 1), coll_above); + get_row_collision_data(Char.curr_row, coll_curr); + get_row_collision_data((int8_t)(Char.curr_row + 1), coll_below); + get_row_collision_data((int8_t)(Char.curr_row - 1), coll_above); /* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка, * в которой флаг перешёл 0→1. */ for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --col) { @@ -1406,12 +1452,12 @@ static void check_collisions(void) * Побочно оставляет тайл в g_curr_tile (как curr_tile2 у оригинала). */ static uint8_t is_obstacle_at_col(int8_t col) { - uint8_t t = get_tile(col, Kid.curr_row); + uint8_t t = get_tile(col, Char.curr_row); if (t == TILE_POTION) return 0; /* зелье не бампит */ - if (t == TILE_GATE) return (uint8_t)!gate_passable(col, Kid.curr_row); + if (t == TILE_GATE) return (uint8_t)!gate_passable(col, Char.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); + pop_trob_modif(g_room)[Char.curr_row * 10 + col] == 2); /* ветка «прыжок сквозь зеркало» (seg004:239) — уровень 4, не портируем */ return 1; } @@ -1424,16 +1470,16 @@ static uint8_t is_obstacle_at_col(int8_t col) 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); + if (Char.alive >= 0) return; /* труп не бампит */ + Char.x = (uint8_t)((int)Char.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); + ct = get_tile((int8_t)(tcol - 1), Char.curr_row); } else { /* толчок ВПРАВО */ if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL) - ct = get_tile((int8_t)(tcol + 1), Kid.curr_row); + ct = get_tile((int8_t)(tcol + 1), Char.curr_row); } if (tile_is_floor(ct)) bumped_floor(); else bumped_fall(); @@ -1447,7 +1493,7 @@ 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 (Char.direction != 0) return; /* (меча в руке у нас нет) */ if (!is_obstacle_at_col(col)) return; /* Прежде чем удариться о стену — попытка зацепиться за её верх * (seg004:0205, то же улучшение). Не вышло — тайл препятствия надо @@ -1469,7 +1515,7 @@ 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 (Char.direction >= 0) return; if (!is_obstacle_at_col(col)) return; if (POP_ENABLE_JUMP_GRAB && pop_ctrl_shift_held()) { /* seg004:0186 */ if (check_grab_run_jump()) return; @@ -1481,6 +1527,61 @@ static void check_bumped_look_left(void) bumped(coll_left - wall_dr[wt] + TILE_RIGHTX - coll_xl, 0, col); } +/* dist_from_wall_behind (seg004:0607) — зеркало dist_from_wall_forward: та + * же арифметика, но от ЗАДНЕЙ грани габарита (лицом вправо задняя грань — + * левая, и наоборот). Нужна только check_guard_bumped. */ +static int dist_from_wall_behind(uint8_t tiletype, int8_t tcol) +{ + uint8_t type = wall_type(tiletype); + int coll_left; + if (type == 0) return 99; + coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + if (Char.direction >= 0) /* лицом вправо */ + return coll_xl - (coll_left + TILE_RIGHTX - wall_dr[type]); + return wall_dl[type] + coll_left - coll_xr; /* лицом влево */ +} + +/* check_guard_bumped (seg004:0522) — стражья замена check_bumped, и она + * НЕ про «упереться на бегу»: срабатывает, только когда персонажа с + * ВЫНУТЫМ МЕЧОМ теснят спиной в стену (его собственный шаг назад в бою или + * наступающий Кид). Отсюда и проверки: action = run_jump, жив, меч в руке, + * а расстояние меряется от ЗАДНЕЙ грани. + * + * Порядок проб как в оригинале, включая побочный эффект: если тайл под + * персонажем не подошёл и он смотрит вправо, оригинал перечитывает тайл + * СЛЕВА (`--tile_col`), и дальше is_obstacle/dist_from_wall_behind считают + * уже по нему. */ +static void check_guard_bumped(void) +{ + int8_t col = Char.curr_col, row = Char.curr_row; + uint8_t t, hit; + int delta_x; + + if (Char.action != ACT_RUNJUMP || Char.alive >= 0 || + Char.sword < SWORD_2_DRAWN) return; + + t = get_tile_at_char(); + hit = (uint8_t)(t == TILE_WALL || t == TILE_DOORTOP_FLOOR || + (t == TILE_GATE && !gate_passable(col, row))); + if (!hit && Char.direction >= 0) { + col--; + t = get_tile(col, row); + hit = (uint8_t)(t == TILE_DOORTOP_FLOOR || + (t == TILE_GATE && !gate_passable(col, row))); + } + if (!hit) return; + + set_char_collision(); + if (!is_obstacle_at_col(col)) return; + delta_x = dist_from_wall_behind(g_curr_tile, col); + if (delta_x < 0 && delta_x > -13) { + Char.x = (uint8_t)char_dx_forward(-delta_x); + pop_char_set_seq(SEQ_65_BUMP_FWD_SWORD); + play_seq(); + pop_load_fram_det_col(); + } +} + /* check_bumped (seg004:029D). */ static void check_bumped(void) { @@ -1490,12 +1591,12 @@ static void check_bumped(void) * 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; /* кадры подъёма/спуска */ + if (Char.action == ACT_HANGCLIMB || Char.action == ACT_HANGSTRAIGHT) return; + if (Char.frame >= 135 && Char.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; + if (Char.frame >= 87 && Char.frame < 100) return; if (bump_col_l != BUMP_NONE) check_bumped_look_right(); else if (bump_col_r != BUMP_NONE) check_bumped_look_left(); } @@ -1522,7 +1623,7 @@ static void make_loose_fall(int pos, uint8_t modifier) * frame 79 (прыжок вверх) с loose СВЕРХУ → пробить сверху. */ static void check_press(void) { - uint8_t frame = Kid.frame, action = Kid.action; + uint8_t frame = Char.frame, action = Char.action; uint8_t above_press = 0; /* «нажимается» тайл СВЕРХУ */ /* Кадры виса (87..99) и начала подъёма (135..140): нажимается тайл, за * КОТОРЫЙ ПЕРСОНАЖ ДЕРЖИТСЯ, т.е. верхний (seg006:1688). Отсюда падение @@ -1536,11 +1637,11 @@ static void check_press(void) return; /* seg006:1704 — прочее не жмёт */ } if (above_press) { - if (Kid.curr_row == 0) { + if (Char.curr_row == 0) { /* Ряд 0: «сверху» — ряд 2 комнаты СВЕРХУ (в оригинале get_tile с * row=−1 уходит по links.up, find_room_of_tile seg006:005D). * Плита-ПОТОЛОК; кнопок на потолке в этом тесте нет. */ - int8_t c = Kid.curr_col; + int8_t c = Char.curr_col; if (g_above && g_link_u && c >= 0 && c <= 9 && (g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) && (int8_t)pop_ceil_modif[c] <= 0) { @@ -1548,7 +1649,7 @@ static void check_press(void) is_guard_notice = 1; /* seg006:1734 */ } } else if (get_tile_above_char() == TILE_LOOSE) { - make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1); + make_loose_fall((Char.curr_row - 1) * 10 + Char.curr_col, 1); is_guard_notice = 1; /* seg006:1734 */ } return; @@ -1560,7 +1661,7 @@ static void check_press(void) if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return; { uint8_t t = get_tile_at_char(); - uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); + uint8_t tp = (uint8_t)(Char.curr_row * 10 + Char.curr_col); if (t == TILE_OPENER || t == TILE_CLOSER) pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]); else if (t == TILE_LOOSE) { @@ -1599,7 +1700,7 @@ static void do_knock(int tile_row) void pop_check_knock(void) __banked { if (knock) { - do_knock(Kid.curr_row - (knock > 0)); + do_knock(Char.curr_row - (knock > 0)); knock = 0; } } @@ -1633,21 +1734,20 @@ void pop_loose_reset(void) __banked static void fell_on_your_head(void) { - uint8_t frame = Kid.frame, action = Kid.action; + uint8_t frame = Char.frame, action = Char.action; if ((frame < 5 || frame >= 15) && (action < ACT_HANGCLIMB || action == ACT_TURN)) { - Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; - pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */ - if (take_hp(1)) { - kid_set_seq(SEQ_22_CRUSHED); - pop_kid_play(); + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + if (pop_take_hp(1)) { /* «брызги» даст pop_do_delta_hp */ + pop_char_set_seq(SEQ_22_CRUSHED); + play_seq(); determine_col(); - pop_kid_dead = 1; + if (Char.charid == CHARID_0_KID) pop_kid_dead = 1; } else if (frame != FRAME_109_CROUCH) { if (get_tile_behind_char() == TILE_EMPTY) - Kid.x = (uint8_t)char_dx_forward(-2); - kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID); - pop_kid_play(); + Char.x = (uint8_t)char_dx_forward(-2); + pop_char_set_seq(SEQ_52_LOOSE_FELL_ON_KID); + play_seq(); determine_col(); } } @@ -1660,9 +1760,16 @@ static void check_loose_fall_on_kid(void) int mcol, my; if (pop_kid_dead) return; if (!pop_loose_mob_pos(&mcol, &my)) return; - if (Kid.curr_col != (int8_t)mcol) return; - if (my < (int)Kid.y && (int)Kid.y - 30 < my) + /* Своё окно Char, как в оригинале (seg007:1199 — loadkid/savekid внутри + * самой функции): зовут нас из pop_loose_tick, а тот идёт в главном + * цикле вне окон, и в Char к этому моменту лежит СТРАЖ (он тикает + * последним). Плита на голову — про Кида. */ + pop_loadkid(); + if (Char.curr_col == (int8_t)mcol && + my < (int)Char.y && (int)Char.y - 30 < my) { fell_on_your_head(); + pop_savekid(); + } } /* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ @@ -1774,7 +1881,7 @@ void pop_loose_tick(void) __banked * поднимающегося по лестнице — срезать сам спрайт. */ int pop_clip_char_right(void) __banked { - if (Kid.frame >= 224 && Kid.frame < 229) + if (Char.frame >= 224 && Char.frame < 229) return pop_leveldoor_right; return 0; } @@ -1793,10 +1900,10 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked * SDLPoP говорит «frames 217..228», а КОД проверяет * `>= frame_224_exit_stairs_8`, то есть 224..228 — портируем по коду. * Правую кромку отдаёт pop_clip_char_right (парная половина ветки). */ - if (Kid.frame >= 224 && Kid.frame < 229) + if (Char.frame >= 224 && Char.frame < 229) return pop_leveldoor_ybottom + 1; - if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ + if (Char.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ xr = xl + wh; /* char_x_right */ top_y = obj_y - (int)h + 1; /* char_top_y */ if (top_y >= 192) top_y = 0; /* весь спрайт ниже комнаты */ @@ -1807,12 +1914,12 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked t = get_tile(cL, trow); if (t != TILE_WALL && !tile_is_floor(t)) return 0; /* кадр 79 (прыжок вверх) / 81 (зацеп) в стойке клипятся и по одной грани */ - if (!(Kid.action == 0 && (Kid.frame == 79 || Kid.frame == 81))) { + if (!(Char.action == 0 && (Char.frame == 79 || Char.frame == 81))) { t = get_tile(cR, trow); if (t != TILE_WALL && !tile_is_floor(t)) return 0; } - clip_row = (int8_t)(Kid.curr_row + 1); + clip_row = (int8_t)(Char.curr_row + 1); if (clip_row < 0 || clip_row > 4) return 0; clip_y = y_clip[clip_row]; /* ряд 0 — клип безусловный; иначе только когда линия реально режет спрайт */ @@ -1834,17 +1941,17 @@ static void check_leave_below(void) * подтягивании y ≈ 248, и проверка «ниже комнаты» (y>=211) иначе * перехватывает подъём и роняет Kid из уровня. * goto_other_room(up): y += 189, curr_row = y_to_row_mod4. */ - if (g_link_u && Kid.action != ACT_BUMPED && Kid.action != ACT_FREEFALL && - Kid.action != ACT_MIDAIR && - (int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) { - Kid.y = (uint8_t)(Kid.y + 189); - Kid.curr_row = pop_y_to_row((int16_t)Kid.y); + if (g_link_u && Char.action != ACT_BUMPED && Char.action != ACT_FREEFALL && + Char.action != ACT_MIDAIR && + (int8_t)Char.y < 10 && (int8_t)Char.y > -16) { + Char.y = (uint8_t)(Char.y + 189); + Char.curr_row = pop_y_to_row((int16_t)Char.y); pop_leave_dir = 3; return; } - if (Kid.y >= 211) { - Kid.y = (uint8_t)(Kid.y - 189); - Kid.curr_row = pop_y_to_row((int16_t)Kid.y); + if (Char.y >= 211) { + Char.y = (uint8_t)(Char.y - 189); + Char.curr_row = pop_y_to_row((int16_t)Char.y); pop_fell_out = 1; } } @@ -1859,35 +1966,35 @@ static void check_leave_below(void) static void check_leave(void) { int fx; - uint8_t f = Kid.frame; + uint8_t f = Char.frame; /* seg002 leave_room: комнату НЕ покидаем во время разворота (action turn), * подъёма-с-зацепа (climb, кадры 135..149) и вставания из приседа * (110..119). Без этого гарда разворот у правого шва (взгляд флипается * право→лево, char_x_left проскакивает >=198) телепортирует Kid сквозь * закрытые ворота в соседнюю комнату (leave_room возвращает −1). */ - if (Kid.action == ACT_TURN || + if (Char.action == ACT_TURN || (f >= 135 && f < 150) || (f >= 110 && f < 120)) return; /* Передний край — ТОЛЬКО через char_x_forward_edge (obj_x-арифметика * set_char_collision): в зоне шва (<=57) он на пиксель правее чистого - * Kid.x+dx, и Kid, осевший у ворот на x=57, порога leave-left НЕ достигает + * Char.x+dx, и Kid, осевший у ворот на x=57, порога leave-left НЕ достигает * (эталон SDLPoP: экран не переключается, BUG-SEAM-PINGPONG). */ fx = char_x_forward_edge(); - if (Kid.direction >= 0) { /* взгляд вправо: fx = char_x_right */ - uint8_t t9 = get_tile(9, Kid.curr_row); + if (Char.direction >= 0) { /* взгляд вправо: fx = char_x_right */ + uint8_t t9 = get_tile(9, Char.curr_row); if (g_link_r && fx >= 201 && t9 != TILE_DOORTOP_FLOOR && t9 != TILE_DOORTOP) { /* leave_room (seg002): под doortop у правой кромки выхода нет */ - Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2; + Char.x = (uint8_t)(Char.x - 140); determine_col(); pop_leave_dir = 2; } else if (g_link_l && fx <= 57) { - Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1; + Char.x = (uint8_t)(Char.x + 140); determine_col(); pop_leave_dir = 1; } } else { /* взгляд влево: fx = char_x_left */ if (g_link_l && fx <= 54) { - Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1; + Char.x = (uint8_t)(Char.x + 140); determine_col(); pop_leave_dir = 1; } else if (g_link_r && fx >= 198) { - Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2; + Char.x = (uint8_t)(Char.x - 140); determine_col(); pop_leave_dir = 2; } } } @@ -1926,19 +2033,22 @@ static int is_spike_harmful(uint8_t tilepos) } /* spiked (seg005:0888): напоролся — пика становится безвредной (0xFF), - * выровнять по колонке, take_hp(100) = смерть, seq_51 (импейл). */ + * выровнять по колонке, pop_take_hp(100) = смерть, seq_51 (импейл). */ static void spiked(uint8_t tilepos) { pop_trob_modif(g_room)[tilepos] = 0xFF; - Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; - Kid.x = (uint8_t)(pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + 10); - Kid.x = (uint8_t)char_dx_forward(8); - Kid.fall_y = 0; - take_hp(100); /* (TODO: sound_48_spiked) */ - kid_set_seq(SEQ_51_SPIKED); - pop_kid_play(); + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + Char.x = (uint8_t)(pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10); + Char.x = (uint8_t)char_dx_forward(8); + Char.fall_y = 0; + pop_take_hp(100); /* (TODO: sound_48_spiked) */ + pop_char_set_seq(SEQ_51_SPIKED); + play_seq(); determine_col(); - pop_kid_dead = 1; + /* pop_kid_dead — НАШ флаг респавна, он про Кида. У прочих персонажей + * смерть видна по обнулившимся HP: play_guard ставит alive = 0, а + * дальше их доигрывает общий control() (seq_71_dying). */ + if (Char.charid == CHARID_0_KID) pop_kid_dead = 1; } /* check_spiked (seg006:0658): под персонажем пика И вредна И кадр @@ -1946,12 +2056,12 @@ static void spiked(uint8_t tilepos) * (43/26) при harmful!=0 → smert'. */ static void check_spiked(void) { - uint8_t frame = Kid.frame, tp; + uint8_t frame = Char.frame, tp; int h; - if (Kid.curr_col < 0 || Kid.curr_col > 9 || Kid.curr_row < 0 || Kid.curr_row > 2) + if (Char.curr_col < 0 || Char.curr_col > 9 || Char.curr_row < 0 || Char.curr_row > 2) return; if (get_tile_at_char() != TILE_SPIKE) return; - tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); + tp = (uint8_t)(Char.curr_row * 10 + Char.curr_col); h = is_spike_harmful(tp); if ((h >= 2 && ((frame >= FRAME_7_RUN && frame < 15) || (frame >= FRAME_34_RUNJUMP_START && frame < 40))) || @@ -1972,12 +2082,12 @@ static void check_spike_below(void) int xl = kid_fp_obj_x() / 2 + 58; /* char_x_left */ int xr; int8_t c0, c1, col; - if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ + if (Char.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ xr = xl + wh; /* char_x_right */ c0 = get_tile_div_mod_m7(xl); c1 = get_tile_div_mod_m7(xr); for (col = c0; col <= c1; col++) { - int8_t row = Kid.curr_row; + int8_t row = Char.curr_row; if (col < 0 || col > 9) continue; for (;;) { uint8_t t = get_tile(col, row); @@ -1988,9 +2098,15 @@ static void check_spike_below(void) } } -void pop_phys_tick(void) __banked +/* ---- Кадровая физика: одна и та же над Char, два входа --------------- * + * Оригинал держит физику ОДНИМ набором функций над Char, а кто именно в + * Char — решают окна loadkid/savekid и loadshad/saveshad (seg006:809). + * Мы теперь так же: kid_phys/guard_phys — это списки вызовов из + * play_kid_frame (seg000:1209) и play_guard_frame (seg000:1246), а сами + * проверки общие. Списки отличаются ровно тем, чем в оригинале. */ + +static void kid_phys(void) { - if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */ if (grab_timer) grab_timer--; fall_accel(); fall_speed(); @@ -2015,3 +2131,54 @@ void pop_phys_tick(void) __banked check_leave(); /* seg002: уход вбок → переход в соседнюю комнату */ } } + +/* play_guard_frame (seg000:1252..1266). Чего у стража НЕТ по сравнению с + * Кидом: bump_into_opponent, check_collisions/check_bumped (у него свой + * check_guard_bumped), check_gate_push, check_knock и весь уход из комнаты. + * check_chomped_guard появится вместе с чомперами (L3-CHOMP). */ +static void guard_phys(void) +{ + fall_accel(); + fall_speed(); + determine_col(); + check_guard_bumped(); + check_action(); + check_press(); /* страж тоже давит кнопки и проваливает loose */ + check_spike_below(); + check_spiked(); +} + +/* Физика КИДА — своё окно Char: главный цикл зовёт нас вне окон, а физика + * работает с активным персонажем. Сохраняем ПОЛНОСТЬЮ (pop_savekid_and_opp): + * физика меняет персонажу всё — координаты, ряд/колонку, кадр, seq. */ +void pop_phys_tick(void) __banked +{ + if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */ + pop_loadkid_and_opp(); + kid_phys(); + pop_savekid_and_opp(); +} + +/* Физика СТРАЖА. Зовётся из главного цикла (резидент), а не из guards.c: + * цена та же — один трамплин на кадр, — но банк 1 не зовёт банк 3, а окно + * Char стоит копию 16 байт. */ +void pop_guard_phys_tick(void) __banked +{ + if (Guard.charid == 0 || Guard.room != g_room) return; + pop_loadshad_and_opp(); + /* seg000:1254 — физика работает, ТОЛЬКО пока страж внутри комнаты: за + * пределами полосы персонаж просто перестаёт двигаться, а + * провалившегося ниже убирает check_guard_fallout (seg002:0241). + * + * Это значит «своим ходом из комнаты не уйдёт», но НЕ «остаётся в ней + * всегда»: когда комнату меняет КИД, exit_room (seg002:03C7) может + * перенести стража следом — follow_guard, если тот жив, с вынутым + * мечом, стоит у нужного края (x < 91 при уходе влево, x >= 165 при + * уходе вправо) и в целевой комнате нет своего живого стража. У нас + * follow_guard пока не портирован (GUARD-PHYS в TASKS_OPEN.md). */ + if (Char.x >= 44 && Char.x < 211) { + pop_load_fram_det_col(); /* load_frame_to_obj + load_fram_det_col */ + guard_phys(); + } + pop_saveshad_and_opp(); +} diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 5e91f3b..b280ba7 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -19,12 +19,21 @@ * что и в pop_room_draw, чтобы отрисовка видела изменения. */ void pop_map_set(uint8_t *fg) __banked; -/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии: - * lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2) - * в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая - * колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */ -void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, - const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked; +/* Связи комнаты (0=нет соседа; иначе номер). Заодно кэширует fg соседей + * слева и справа ЦЕЛИКОМ (читает уровень сам). Оригинал добирается до любой + * колонки через find_room_of_tile, у нас уровень в EMM-странице и маппить её + * на каждый get_tile (горячий путь физики) дорого — поэтому копия соседей. + * Колонка персонажа считается из x и на диапазоне x 0..255 лежит в −5..13, + * то есть заведомо выходит за комнату; кромочных колонок не хватало, и + * get_tile отдавал мнимую стену (BUG-SWORD-GHOST-1). Звать в enter_room. */ +void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked; + +/* check_skel: тайл скелета -> пол + пометка перерисовки (обе страницы). */ +void pop_skel_wake_tile(uint8_t tilepos) __banked; + +/* Openness ворот в (col,row) с резолвом за край комнаты (соседи слева/справа). + * 0xFF — тайла нет. Нужен лучу видимости стража (guards.c). */ +uint8_t pop_gate_modif(int8_t col, int8_t row) __banked; /* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob). * Звать в enter_room. */ @@ -125,9 +134,26 @@ extern uint8_t pop_fell_out; /* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed + * determine_col + check_action (триггер падения / do_fall / приземление). - * Порядок как в оригинале (seg000 play_kid_frame). */ + * Порядок как в оригинале (seg000 play_kid_frame). + * + * Физика внутри работает с АКТИВНЫМ персонажем (Char), как в оригинале, и + * сама открывает окно loadkid/savekid — звать вне окон Char. */ void pop_phys_tick(void) __banked; +/* То же для стража — порт хвоста play_guard_frame (seg000:1246). Звать из + * главного цикла СРАЗУ после pop_guard_tick() (тот прокручивает ИИ и + * последовательность), тоже вне окон Char: функция открывает своё. + * + * Физика у стража та же самая (одни функции над Char), отличается только + * список вызовов: нет ухода из комнаты, нет check_knock, вместо check_bumped + * — check_guard_bumped. Тот же вход обслужит скелета/тень/визиря, когда + * они появятся: в оригинале все они живут в слоте Guard. + * + * «Нет ухода из комнаты» — про СВОЙ ход. Следом за Кидом стража переносит + * exit_room/follow_guard (seg002:03C7/039E) — отдельная механика, у нас не + * портирована (GUARD-PHYS). */ +void pop_guard_phys_tick(void) __banked; + /* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл * над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid, * ушедший головой под пол при спуске/висе, не просвечивает сквозь щели). diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index e0343a9..14d2905 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -14,6 +14,12 @@ #include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */ uint8_t pop_loose_landed; +uint8_t pop_droppedout; +/* leveldoor_open (seg007:456): дверь уровня доехала до верха. Ставит + * анимация двери (pop_trob, банк 6), читает спецсобытие подъёма скелета + * (guards.c, банк 1) — снова случай «данные через границу банков». + * Сбрасывается стартом уровня (seg003:97). */ +uint8_t pop_leveldoor_open; /* ---- Шина управления (seg006 globals) ------------------------------ * * Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка — diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h index 1660c13..f664b32 100644 --- a/applications/PoP/roomtest/pop_state.h +++ b/applications/PoP/roomtest/pop_state.h @@ -29,6 +29,17 @@ extern uint8_t pop_immortal; extern int pop_leveldoor_right; extern int pop_leveldoor_ybottom; +/* droppedout (seg006:1140): Кида СПИХНУЛИ с уступа в бою — он падает из + * боевой стойки. Ставит start_fall (pop_map, банк 3), читает ИИ стража + * (guards.c, банк 1): по этому флагу страж решает, идти ли за упавшим вниз + * (guard_follows_kid_down, seg002:09F8). Отсюда и место в общем модуле — + * банки друг другу данные не отдают. */ +extern uint8_t pop_droppedout; + +/* leveldoor_open (seg007:456): 1 — дверь уровня открыта полностью. Пишет + * анимация двери (банк 6), читает подъём скелета (банк 1). */ +extern uint8_t pop_leveldoor_open; + /* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c. * Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на * _pop_dbg_trap (резидент W1, адрес однозначен). */ diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 590dbca..382ca5f 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -4,6 +4,7 @@ * См. pop_trob.h. */ #include "pop_trob.h" +#include "pop_state.h" /* pop_leveldoor_open */ #include "pop_level.h" #include "pop_geom.h" #include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */ @@ -328,7 +329,7 @@ static void animate_leveldoor(uint8_t *m, int8_t *type) else *m = (uint8_t)(*m - sp); } else { if (*m < 43) (*m)++; - if (*m >= 43) { *m = 43; *type = -1; } + if (*m >= 43) { *m = 43; *type = -1; pop_leveldoor_open = 1; } } } diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h index 94ad713..f4a9879 100644 --- a/applications/PoP/roomtest/pop_tune.h +++ b/applications/PoP/roomtest/pop_tune.h @@ -14,7 +14,7 @@ * прятаться числами по трём модулям. * 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа. * - * Пока это только `#define` — ни INI, ни меню нет (см. TASKS.md, запись + * Пока это только `#define` — ни INI, ни меню нет (см. TASKS_OPEN.md, запись * TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг * заводится ЗДЕСЬ, а не константой по месту. Имена — как у SDLPoP * (custom->имя), чтобы сверка с оригиналом оставалась механической. diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index bee8669..8f60500 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -74,7 +74,7 @@ uint8_t pop_cheats; * теряется примерно в 44 % случаев, поэтому байты копятся и один из * полусотни пропадает ДО чтения порта: нажатие «не срабатывает». * - * Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько + * Что НЕ помогает (замерено, TASKS_CLOSED.md/KBD-1): редкий опрос (несколько * вызовов за кадр — они попадают туда, где прерывания и так разрешены, и * лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще * без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя — машина @@ -98,7 +98,9 @@ static void kbd_idle(void) #define ROOM_1 1 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ -#define FIRST_LEVEL 1 /* с какого уровня начинается игра */ +#ifndef FIRST_LEVEL +#define FIRST_LEVEL 1 /* с какого уровня начинается игра (-DFIRST_LEVEL=N) */ +#endif #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ @@ -259,16 +261,25 @@ static void flash_bg(uint8_t r, uint8_t g, uint8_t b) /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */ -static void enter_room(uint8_t room) +/* side — куда ушёл КИД (0 left, 1 right, 2 up, 3 down); 0xFF = «не переход + * за Кидом» (старт уровня, респавн, чит-навигация). От него зависит ровно + * одно: пойдёт ли страж следом (pop_guard_follow, порт exit_room+ + * follow_guard) или останется в прежней комнате. */ +static void enter_room_side(uint8_t room, uint8_t side) { uint8_t pg; - pop_guard_leave(); /* запомнить стража уходящей комнаты */ + /* Страж идёт за Кидом? — решаем ДО того, как что-либо загрузим: функции + * нужны и старая комната (Guard.room), и целевая. Пошёл — ни leave, ни + * enter звать нельзя: он уже перенесён и живёт дальше как есть. */ + uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0); + if (!follow) 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); pop_map_set(room_fg); + /* Соседей слева/справа карта забирает из уровня САМА (целиком, см. + * pop_map_set_edges) — отдельной копии в приложении нет. */ pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), - pop_room_link(room, 2), pop_room_link(room, 3), - lcol_fg, rcol_fg); + pop_room_link(room, 2), pop_room_link(room, 3)); pop_room_set_below(below_fg); /* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика * уровня, mod — живой room_modif (ленивая инициализация). */ @@ -296,7 +307,8 @@ static void enter_room(uint8_t room) pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ - pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ + if (!follow) + pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ * per-room (room_modif живёт между входами); рисуем и коллидируем по ним, @@ -325,6 +337,10 @@ static void enter_room(uint8_t room) pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */ } +/* Вход в комнату БЕЗ перехода за Кидом: старт уровня, респавн, чит-обход. + * Страж такой смены не «видит» — он остаётся в своей комнате. */ +static void enter_room(uint8_t room) { enter_room_side(room, 0xFF); } + /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos * (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и * для запуска, и для рестарта после смерти / выпадения из уровня: в @@ -378,6 +394,7 @@ static uint8_t pop_checkpoint; static void pop_start_level(void) { + pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ uint8_t entry_pose = pop_level_entry_pose(pop_current_level); uint8_t start_room = pop_level_start_room(); uint8_t pos = pop_level_start_pos(); @@ -444,6 +461,7 @@ int main(void) * по умолчанию (без мерцания); SPACE переключает в однобуфер для * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; + uint8_t autofrz = 0; /* ВРЕМЕННО: автостоп на падении уже сработал */ uint16_t dead_frames = 0; #ifdef ROOMNAV uint8_t nav_prev = 0; @@ -525,6 +543,22 @@ int main(void) * стоит, можно спокойно разбирать позу Kid и окклюзию. */ if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x1E)) frozen = 0; + /* ВРЕМЕННО: автостоп на падении соперника в провал — сцену «скелет + * просвечивает сквозь кладку» руками не поймать (падение длится + * несколько кадров). Замирает сам, дальше снимок и чтение памяти. + * + * Срабатывает ОДИН раз на падение: пока стоит фриз, состояние + * персонажа не меняется, поэтому условие остаётся истинным — без + * этого флага снятие фриза клавишей '2' тут же гасилось повторным + * автостопом в той же итерации. Взводится заново, когда падение + * кончилось (страж убран или вышел из freefall). + * Снять вместе с разбором. */ + if (Guard.charid != 0 && Guard.action == 4 /* in_freefall */ && + Guard.y > 150) { + if (!autofrz) { frozen = 1; autofrz = 1; } + } else { + autofrz = 0; + } { /* Читы (seg000: switch под if (cheats_enabled)). Все — по * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр. * K — убить стража; @@ -592,6 +626,14 @@ int main(void) ((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1)); if (nav && !nav_prev) { uint8_t r = cur_room, i, c, pos = 20, any = 0xFF; + /* Кид МЁРТВ: сначала честный рестарт уровня, потом телепорт. + * Навигация только переставляет Кида (kid_init + сброс HP), + * то есть «оживляет» его мимо start_level — а тот обязан + * вернуть тайлы, модификаторы и ОЧИСТИТЬ слот соперника + * (seg003:100: Guard.direction = dir_56_none). Без этого + * после смерти оставался живой скелет рядом с вернувшейся + * кучей костей. */ + if (pop_kid_dead) { pop_start_level(); dead_frames = 0; } r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1) : (uint8_t)(r == 1 ? 24 : r - 1); enter_room(r); @@ -616,6 +658,7 @@ int main(void) back = dbuf ? (uint8_t)(front ^ 1) : 0; gfx_set_draw_page(back); + pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_ctrl_tick(); /* ввод -> control(): смена seq */ kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ @@ -625,11 +668,13 @@ int main(void) kid_tick(); /* play_seq: следующий кадр */ pop_phys_tick(); /* K3: падение/приземление/стена */ pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */ + pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */ /* Боёвка, порядок как в play_frame (seg000:872): сначала пометить * попадания с обеих сторон, потом применить, потом свести HP. */ pop_check_sword_hurting(); pop_check_sword_hurt(); pop_do_delta_hp(); + pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ uint8_t down = pop_room_link(cur_room, LINK_DOWN); pop_fell_out = 0; @@ -643,6 +688,7 @@ int main(void) } else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */ uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1)); uint8_t left = (uint8_t)(pop_leave_dir == 1); + uint8_t pop_leave_dir_prev = pop_leave_dir; pop_leave_dir = 0; /* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал * зовёт его из leave_room ДО goto_other_room, то есть Char.room @@ -662,7 +708,9 @@ int main(void) hitp_beg_lev = hitp_max; } if (nb) { - enter_room(nb); + /* side для pop_guard_follow — то же направление, по которому + * ищем соседа (roomleave_result оригинала). */ + enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1)); gfx_set_draw_page(back); } } @@ -705,6 +753,13 @@ int main(void) if (pop_level_load_num(pop_next_level) != 0) { pop_next_level = pop_current_level; /* файла нет — остаёмся */ } else { + /* Набор спрайтов соперника у каждого уровня СВОЙ + * (load_lev_spr, seg000:1092): уровень 3 — скелет, у него и + * кадров меньше, чем у стража. Без перезагрузки соперник + * рисуется чужим атласом: часть индексов уходит за его + * пределы, и персонаж просто не виден. */ + pop_guard_free(); + pop_guard_load(); /* Всё состояние прошлого уровня сносит сам pop_start_level: * pop_trob_reset обнуляет room_seen (модификаторы комнат * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают @@ -783,6 +838,13 @@ int main(void) PROF(6); /* kid_draw (тот же циан) */ kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ if (g_after) pop_guard_draw(); + /* Брызги стража рисует сам pop_guard_draw (порядок objtable), а + * флаг снимаем здесь: стража могло не оказаться в отрисованной + * комнате — тогда брызги просто пропадают, как в оригинале. + * Снимаем ПО ФАКТУ, а не безусловно: запись в кадре удара — + * готовая точка останова для отладки в MAME (watchpoint), и + * лишней записи каждый кадр нет. */ + if (pop_guard_hurt) pop_guard_hurt = 0; } pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */ if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index 2b6cc06..40da43b 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -8,6 +8,11 @@ ENGINE_DIR := $(abspath $(CURDIR)/..) # мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код # набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88). CODE_LOC := 0x1000 +# Данные — выше 0x8000: набор t_phys линкует уже полдвижка (геометрия, Kid, +# карта/физика, управление, страж) и его код перевалил за 0x8000. Раньше +# это проявлялось как «тест зациклился»: хвост кода ложился поверх _DATA. +# 0x9000 оставляет ~24 КБ под код и ~28 КБ до стека (0xFEF0). +DATA_LOC := 0xA000 # Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/ # sprinter: под тестом эти функции заглушены (stubs.c), но объявления @@ -17,9 +22,13 @@ EXTRA_INC := -I$(PROJ_ROOT)/libc/include -I$(PROJ_ROOT)/libbgi/include -I$(CURD OBJS_geom := build/eng_pop_geom.rel +# pop_guard.c линкуется НАСТОЯЩИЙ, а не заглушкой: после перевода физики на +# Char урон идёт через pop_take_hp, и кому именно он достанется (hitp_* или +# guardhp_*) решает charid — то есть ровно та логика, которую тесты обязаны +# проверять, а не имитировать. OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \ build/eng_pop_map.rel build/eng_pop_ctrl.rel \ - build/eng_pop_state.rel \ + build/eng_pop_state.rel build/eng_pop_guard.rel \ build/stubs.rel build/scene.rel build/kid_blob.rel \ build/bank_stub.rel @@ -27,6 +36,8 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \ OBJS_grab := $(OBJS_phys) # t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же. OBJS_wall := $(OBJS_phys) +# t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень). +OBJS_char := $(OBJS_phys) include $(TESTKIT)/host-tests.mk diff --git a/applications/PoP/roomtest/tests-host/README.md b/applications/PoP/roomtest/tests-host/README.md index 5ca4259..bfe5b72 100644 --- a/applications/PoP/roomtest/tests-host/README.md +++ b/applications/PoP/roomtest/tests-host/README.md @@ -15,6 +15,7 @@ make -C applications/PoP/roomtest/tests-host t_geom |-------|--------|---------------| | `t_geom` | `pop_geom.c` | геометрия комнаты (`x_bump`, `y_land`, `y_to_row`) и PRNG оригинала | | `t_phys` | `pop_map.c` + `pop_kid.c` + `pop_ctrl.c` | покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал | +| `t_char` | те же + `pop_guard.c` | физика над `Char` для РАЗНЫХ персонажей: кому достаётся урон (`take_hp` по charid), ветки `land()` (Кид −1 HP с двух рядов, страж гибнет, тень цела), падение и гард по X у слота Guard, `check_guard_fallout`, «окна Char не путают персонажей» | Главный тест в `t_geom` — `geom_lcg_matches_reference`. LCG оригинала (`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме @@ -36,6 +37,21 @@ asm с наивной 32-битной формулой на 128 шагах — написано, что и почему поменялось. Молча обновлённый эталон обесценивает весь набор. +Своё дело набор сделал: перенос физики на `Char` (GUARD-PHYS, 2026-08-07) +прошёл при **неизменных трассах** — 1723 проверки без единого расхождения. + +## Грабли раскладки: `DATA_LOC` + +Набор линкует уже полдвижка, и его код перевалил за `--data-loc 0x8000`. +Линкер `sdld` это НЕ ошибка: хвост кода (`_HOME`, деления из libsdcc) он +молча кладёт ПОВЕРХ `_DATA`, после чего тест ведёт себя как «зациклился» +— хотя логика ни при чём. Час на поиск стоило именно это. + +Поэтому: `DATA_LOC` в Makefile набора (сейчас `0x9000`), а `host-tests.mk` +перед каждым прогоном проверяет `CODE_LOC + l__CODE <= DATA_LOC` и падает +с внятным сообщением. Упёрлись снова — поднять `DATA_LOC` (стек в +`crt0_ucsim` начинается с 0xFEF0). + ``` python3 record_golden.py # переснять все трассы python3 record_golden.py 7 9 # только сценарии 7 и 9 diff --git a/applications/PoP/roomtest/tests-host/scene.c b/applications/PoP/roomtest/tests-host/scene.c index 1b333fb..0c306e6 100644 --- a/applications/PoP/roomtest/tests-host/scene.c +++ b/applications/PoP/roomtest/tests-host/scene.c @@ -19,8 +19,8 @@ sc_rec sc_trace[SC_TRACE_MAX]; uint8_t sc_len; static uint8_t sc_fg[30]; -/* Кромки шва: соседей у сцены нет, поэтому по краям стена (тайл 20). */ -static const uint8_t sc_edge[6] = { 20, 20, 20, 20, 20, 20 }; +/* Соседей у сцены нет (links = 0) — по краям комнаты стена: карта сама + * решает так, когда соседняя комната не задана. */ void sc_init(void) { @@ -38,7 +38,7 @@ void sc_room(const uint8_t *fg, uint8_t room) /* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон * и сверху/снизу — тогда решения физики зависят только от sc_fg, и * трасса не поедет, если когда-нибудь поменяется загрузка кромок. */ - pop_map_set_edges(0, 0, 0, 0, sc_edge, sc_edge); + pop_map_set_edges(0, 0, 0, 0); tk_start_room = room; tk_log_clear(); } @@ -53,10 +53,54 @@ void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir) { kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir); Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */ - pop_determine_col(); /* колонку пересчитать под новый X */ + /* Колонку пересчитываем В ОКНЕ Char: determine_col — часть физики, а та + * работает с активным персонажем (порт load_fram_det_col). */ + pop_loadkid(); + pop_determine_col(); + pop_savekid(); pop_kid_hp_reset(); } +/* ---- слот Guard ------------------------------------------------------ */ + +void sc_guard_at(uint8_t charid, uint8_t col, uint8_t row, int8_t dir, uint8_t hp) +{ + /* Поля — как в enter_guard (seg002:0112): X из колонки, Y с пола ряда. */ + Guard.charid = charid; + Guard.curr_col = (int8_t)col; + Guard.curr_row = (int8_t)row; + Guard.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX); + Guard.y = (uint8_t)pop_y_land[row + 1]; + Guard.direction = dir; + Guard.alive = -1; + Guard.action = 1; /* actions_1_run_jump */ + Guard.sword = 2; /* у живого стража меч в руке */ + Guard.fall_x = Guard.fall_y = 0; + Guard.repeat = 0; + Guard.room = 1; + guardhp_max = guardhp_curr = hp; + guardhp_delta = 0; + /* Стойка с мечом (seq_77 у неактивного стража — но нам нужна поза, из + * которой персонаж просто стоит и может упасть). */ + pop_loadshad(); + pop_char_set_seq(77 /* seq_77_guard_stand_inactive */); + play_seq(); + pop_saveshad(); +} + +void sc_guard_run(uint8_t n) +{ + uint8_t i; + for (i = 0; i < n; i++) { + /* play_guard_frame (seg000:1246) без ИИ: окно, кадр, seq, физика. */ + pop_loadshad_and_opp(); + pop_load_fram_det_col(); + play_seq(); + pop_saveshad_and_opp(); + pop_guard_phys_tick(); + } +} + void sc_trace_clear(void) { sc_len = 0; } static void sc_record(void) diff --git a/applications/PoP/roomtest/tests-host/scene.h b/applications/PoP/roomtest/tests-host/scene.h index 5dbe3a0..ba2dafd 100644 --- a/applications/PoP/roomtest/tests-host/scene.h +++ b/applications/PoP/roomtest/tests-host/scene.h @@ -63,6 +63,20 @@ void sc_kid_at(uint8_t col, uint8_t row, int8_t dir); * тайла (нужно, когда исход зависит от того, где именно Кид стоит). */ void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir); +/* ---- Персонажи слота Guard (страж/скелет/тень/визирь) ---------------- * + * Физика у них ОБЩАЯ с Кидом (одни функции над Char, разные окна), поэтому + * тестировать её надо на обоих слотах. Поднимаем персонажа руками, как + * enter_guard (seg002:0112): данных уровня под тестом нет. + * charid — CHARID_2_GUARD / CHARID_1_SHADOW / CHARID_4_SKELETON; + * hp — сколько HP выдать (guardhp_max = guardhp_curr = hp). */ +void sc_guard_at(uint8_t charid, uint8_t col, uint8_t row, int8_t dir, uint8_t hp); + +/* Прогнать n кадров ПЕРСОНАЖА СЛОТА GUARD — порт play_guard_frame без ИИ: + * окно loadshad → load_fram_det_col → play_seq → saveshad, затем + * pop_guard_phys_tick(). ИИ (guards.c, банк 1) в набор не линкуется: он + * решает «куда идти», а тут проверяется «что с ним делает физика». */ +void sc_guard_run(uint8_t n); + /* Прогнать n кадров с ЗАЖАТОЙ маской keys, записывая трассу. * Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */ uint8_t sc_run(uint8_t keys, uint8_t n); diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index 4a40b86..2901f47 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -187,14 +187,77 @@ void pop_clip_sprite(int x, int w, int sy_top, int h) __banked void pop_heal_fast(int x, int y, int w, int h) { (void)x; (void)y; (void)w; (void)h; } -/* ---- 3. состояние стража -------------------------------------------- */ +/* ---- 3. страж ------------------------------------------------------- * + * Само состояние (Guard, guardhp_*, can_guard_see_kid, is_guard_notice…) и + * pop_take_hp даёт НАСТОЯЩИЙ pop_guard.c — он линкуется в набор. Здесь + * только то, что он тянет из уровня: данных уровня под тестом нет, страж + * поднимается вручную (tk_guard_spawn в scene.c). */ -pop_char_t Guard; -kframe pop_gframe; -int8_t can_guard_see_kid; -int8_t guard_refrac, kid_sword_strike; -int8_t offguard, holding_sword; -/* «Кид нашумел» — физика взводит его на ударе о стену, приземлении и - * обрушенной плите (BUG-GUARD-DEAF-1); читает только автопилот стража, - * которого в наборе нет. */ -int8_t is_guard_notice; +uint8_t pop_current_level; /* pop_level.c под тест не линкуется; sc_init ставит 1 */ + +/* Цвет стража — подмена палитры, живёт в pop_gdraw (отрисовка, в набор не + * линкуется). Здесь важен только факт вызова из pop_guard_enter. */ +void pop_guard_set_palette(uint8_t color) __banked { (void)color; } + +/* Связи комнат: нужны check_guard_fallout (возрождение скелета в комнате + * снизу). Тест задаёт их через tk_link_down. */ +uint8_t tk_link_down; +uint8_t pop_room_link(uint8_t room, uint8_t side) +{ (void)room; return side == 3 ? tk_link_down : 0; } + +int8_t pop_level_guard_type(uint8_t level) { (void)level; return 0; } +uint8_t pop_level_guard_hp(uint8_t level) { (void)level; return 3; } + +/* Тайлы «уровня» для соседних комнат: pop_map_set_edges забирает fg соседей + * слева/справа сам (кэш на входе в комнату), значит тесту нужен источник. + * Заполняет сам тест — по умолчанию комнат нет и всё стена. */ +uint8_t tk_room_fg[TK_ROOMS][30]; +void pop_level_access_begin(void) { } +void pop_level_access_end(void) { } +uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos) +{ + if (room == 0 || room >= TK_ROOMS || tilepos >= 30) return 20; /* стена */ + return tk_room_fg[room][tilepos]; +} + +/* Живое состояние стражей по комнатам — как gstate в pop_level.c, только на + * 8 комнат и без данных уровня: тесту нужно ровно то, на что смотрит + * follow_guard (есть ли в целевой комнате свой страж и не труп ли он). + * tk_guard_room != 0 — комната, в которой «по данным уровня» страж есть. */ +uint8_t tk_guard_room; +uint8_t tk_gs_tile[TK_ROOMS]; /* 30 = стража нет (как в pop_level) */ +uint16_t tk_gs_seq[TK_ROOMS]; /* != 0 — запомнен ТРУП */ + +void tk_guard_state_clear(void) +{ + uint8_t i; + for (i = 0; i < TK_ROOMS; i++) { tk_gs_tile[i] = 30; tk_gs_seq[i] = 0; } + tk_guard_room = 0; +} + +uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir, + uint8_t *color, uint8_t *skill) +{ + if (room < 1 || room > TK_ROOMS) return 0; + if (tk_gs_tile[room - 1] < 30) { /* запомненный (в т.ч. труп) */ + *tile = tk_gs_tile[room - 1]; *dir = 0; *color = 0; *skill = 1; + return 1; + } + if (room == tk_guard_room) { /* страж из данных уровня */ + *tile = 11; *dir = 0; *color = 0; *skill = 1; + return 1; + } + return 0; +} +void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x, + uint8_t skill, uint16_t seq, uint8_t dead) +{ + (void)dir; (void)x; (void)skill; + if (room < 1 || room > TK_ROOMS) return; + tk_gs_tile[room - 1] = tile; + tk_gs_seq[room - 1] = dead ? seq : 0; + if (tile >= 30) tk_guard_room = (tk_guard_room == room) ? 0 : tk_guard_room; +} +uint16_t pop_guard_state_seq(uint8_t room) +{ return (room >= 1 && room <= TK_ROOMS) ? tk_gs_seq[room - 1] : 0; } +uint8_t pop_guard_state_x(uint8_t room) { (void)room; return 0; } diff --git a/applications/PoP/roomtest/tests-host/stubs.h b/applications/PoP/roomtest/tests-host/stubs.h index c08e645..2185277 100644 --- a/applications/PoP/roomtest/tests-host/stubs.h +++ b/applications/PoP/roomtest/tests-host/stubs.h @@ -40,4 +40,18 @@ void tk_log_clear(void); /* Сколько записей данного вида в журнале (0 = не было). */ uint8_t tk_log_count(uint8_t kind); + +/* Состояние стражей по комнатам для follow_guard (см. stubs.c). */ +#define TK_ROOMS 8 +extern uint8_t tk_guard_room; /* комната, где страж есть «по данным уровня» */ +extern uint8_t tk_gs_tile[TK_ROOMS]; +extern uint16_t tk_gs_seq[TK_ROOMS]; +void tk_guard_state_clear(void); + +/* Связь «вниз» для check_guard_fallout (скелет возрождается в комнате 3). */ +extern uint8_t tk_link_down; + +/* fg комнат «уровня» — источник для кэша соседей в pop_map_set_edges. */ +extern uint8_t tk_room_fg[TK_ROOMS][30]; + #endif diff --git a/applications/PoP/roomtest/tests-host/t_char.c b/applications/PoP/roomtest/tests-host/t_char.c new file mode 100644 index 0000000..789665d --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_char.c @@ -0,0 +1,460 @@ +/* + * t_char — физика над Char для РАЗНЫХ ПЕРСОНАЖЕЙ. + * + * Зачем отдельный набор. После GUARD-PHYS физика — один набор функций над + * глобалом `Char`, а кто именно в Char, решают окна loadkid/savekid и + * loadshad/saveshad (как в оригинале, seg006:809). В слоте Guard живёт не + * только страж: тем же кодом ходят скелет (ур. 3), тень (4/5/6/12), визирь + * (13) и мышь (8) — `autocontrol_opponent` (seg002:628) разводит их только + * по ИИ, а play_guard_frame у всех общий. Значит расхождения «у Кида так, + * у прочих иначе» — это ВЕТКИ ПО charid внутри общих функций, и проверять + * их надо именно так: одна сцена, разные персонажи. + * + * t_phys страхует, что перенос ничего не сломал КИДУ (характеризационные + * трассы). Здесь — противоположное: что не-Кид получает СВОЮ ветку. + * + * Проверки точечные (значение поля после N кадров), а не трассы: ветки + * различаются исходом, а не траекторией, и эталон тут только запутал бы. + */ +#include + +#include "tcheck.h" +#include "pop_kid.h" +#include "pop_map.h" +#include "pop_guard.h" +#include "pop_geom.h" +#include "pop_state.h" +#include "scene.h" +#include "stubs.h" + +#define E 0 /* empty */ +#define F 1 /* floor */ +#define W 20 /* wall */ + +/* Ровный пол на ряду 1 (как room_flat в t_phys). */ +static const uint8_t room_flat[30] = { + W, E, E, E, E, E, E, E, E, E, + W, F, F, F, F, F, F, F, F, F, + W, W, W, W, W, W, W, W, W, W, +}; + +/* Пол ряда 0 обрывается после колонки 3; ряд 2 — пол. Персонаж, сошедший + * с кромки (0,3), падает ДВА ряда: ровно та высота, на которой ветки + * land() по charid расходятся (seg005:189). */ +static const uint8_t room_drop2[30] = { + W, F, F, F, E, E, E, E, E, E, + W, E, E, E, E, E, E, E, E, E, + W, F, F, F, F, F, F, F, F, F, +}; + +/* ---- take_hp: кому достаётся урон ----------------------------------- */ + +TC_TEST(char_take_hp_goes_to_active) +{ + sc_room(room_flat, 1); + sc_kid_at(3, 1, -1); + sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); + + /* Окно Кида: урон обязан уйти в hitp_delta и не тронуть стража. */ + pop_loadkid(); + hitp_delta = 0; guardhp_delta = 0; + tc_check(pop_take_hp(1), 0, "kid: не последнее HP", __LINE__); + tc_check(hitp_delta, -1, "kid: hitp_delta", __LINE__); + tc_check(guardhp_delta, 0, "kid: страж не тронут", __LINE__); + pop_savekid(); + + /* Окно стража: тот же вызов — уже в guardhp_delta. */ + pop_loadshad(); + hitp_delta = 0; guardhp_delta = 0; + tc_check(pop_take_hp(1), 0, "guard: не последнее HP", __LINE__); + tc_check(guardhp_delta, -1, "guard: guardhp_delta", __LINE__); + tc_check(hitp_delta, 0, "guard: Кид не тронут", __LINE__); + /* Смертельный удар: возврат 1 и дельта на всё оставшееся. */ + guardhp_delta = 0; + tc_check(pop_take_hp(100), 1, "guard: смертельный удар", __LINE__); + tc_check(guardhp_delta, -3, "guard: дельта на все HP", __LINE__); + pop_saveshad(); +} + +/* Чит бессмертия — только Киду: страж обязан гибнуть и с ним (иначе + * отладочный режим ломает боёвку). */ +TC_TEST(char_immortal_covers_kid_only) +{ + sc_room(room_flat, 1); + sc_kid_at(3, 1, -1); + sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); + pop_immortal = 1; + + pop_loadkid(); + hitp_delta = 0; + tc_check(pop_take_hp(100), 0, "kid: бессмертен", __LINE__); + tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__); + pop_savekid(); + + pop_loadshad(); + guardhp_delta = 0; + tc_check(pop_take_hp(100), 1, "guard: чит на него не действует", __LINE__); + pop_saveshad(); + + pop_immortal = 0; +} + +/* ---- do_delta_hp: дельты применяются каждому своему счётчику --------- */ + +TC_TEST(char_delta_hp_applies_per_character) +{ + sc_room(room_flat, 1); + sc_kid_at(3, 1, -1); + sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); + + hitp_delta = -1; + guardhp_delta = -2; + pop_kid_hurt = 0; + pop_guard_hurt = 0; + pop_do_delta_hp(); + + tc_check(hitp_curr, 2, "kid: HP 3-1", __LINE__); + tc_check(guardhp_curr, 1, "guard: HP 3-2", __LINE__); + /* Флаги «брызг» — по одному на персонажа, и только на УРОН. */ + tc_check(pop_kid_hurt, 1, "kid: брызги", __LINE__); + tc_check(pop_guard_hurt, 1, "guard: брызги", __LINE__); + + /* Плюсовая дельта (зелье) брызг и вспышки давать НЕ должна. */ + pop_kid_hurt = 0; + hitp_delta = 1; + pop_do_delta_hp(); + tc_check(hitp_curr, 3, "kid: зелье вернуло HP", __LINE__); + tc_check(pop_kid_hurt, 0, "kid: зелье — не урон", __LINE__); +} + +/* ---- land(): ветки по charid (seg005:173) --------------------------- */ + +/* Кид с двух рядов теряет 1 HP и остаётся жив. */ +TC_TEST(char_kid_survives_two_row_fall) +{ + sc_room(room_drop2, 1); + sc_kid_at(3, 0, 0); + sc_trace_clear(); + sc_run(SC_R, 30); /* шагнуть с кромки и упасть */ + + pop_do_delta_hp(); + tc_check(Kid.curr_row, 2, "kid: приземлился на ряд 2", __LINE__); + tc_check(hitp_curr, 2, "kid: −1 HP за два ряда", __LINE__); + tc_check(pop_kid_dead, 0, "kid: жив", __LINE__); +} + +/* СТРАЖ с той же высоты гибнет: seg005:190 уводит charid_2_guard прямо в + * take_hp(100). Это не «баланс», а буквальная ветка оригинала. */ +TC_TEST(char_guard_dies_from_two_row_fall) +{ + sc_room(room_drop2, 1); + sc_kid_at(1, 2, 0); /* Кид в стороне, не мешает */ + /* Ставим сразу НАД ПУСТОТОЙ: ИИ в наборе нет, сам с кромки страж не + * сойдёт, а физике довольно того, что под ним не пол. Колонка 5, а не + * 4: решение принимается по ВЕСОВОЙ ТОЧКЕ кадра (dx_weight, seg006), + * а она у стойки стража уходит на 9 px назад — с колонки 4 точка + * попадала бы ещё на пол колонки 3. */ + sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); + sc_guard_run(30); + + pop_do_delta_hp(); + tc_check(Guard.curr_row, 2, "guard: приземлился на ряд 2", __LINE__); + tc_check(guardhp_curr, 0, "guard: два ряда = смерть", __LINE__); +} + +/* ТЕНЬ (charid_1_shadow) с тех же двух рядов не теряет ничего: seg005:189 + * уводит её в ветку «упал с одного ряда». */ +TC_TEST(char_shadow_survives_two_row_fall) +{ + sc_room(room_drop2, 1); + sc_kid_at(1, 2, 0); + sc_guard_at(CHARID_1_SHADOW, 5, 0, 0, 3); + sc_guard_run(30); + + pop_do_delta_hp(); + tc_check(Guard.curr_row, 2, "shadow: приземлилась на ряд 2", __LINE__); + tc_check(guardhp_curr, 3, "shadow: HP не тронуто", __LINE__); +} + +/* ---- физика слота Guard вообще работает ----------------------------- */ + +/* До GUARD-PHYS у стража не было ни fall_accel, ни check_action: сойдя с + * кромки, он просто висел в воздухе. Тест ловит именно это. */ +TC_TEST(char_guard_falls_at_all) +{ + uint8_t y0; + sc_room(room_drop2, 1); + sc_kid_at(1, 2, 0); + sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); + y0 = Guard.y; + + sc_guard_run(6); + tc_check(Guard.y > y0, 1, "guard: пошёл вниз", __LINE__); +} + +/* Гард по X (seg000:1254): за пределами комнаты физика молчит — именно так + * оригинал не выпускает стража из комнаты. */ +TC_TEST(char_guard_physics_gated_by_x) +{ + uint8_t y0; + sc_room(room_drop2, 1); + sc_kid_at(1, 2, 0); + sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); + Guard.x = 220; /* правее 211 — вне комнаты */ + y0 = Guard.y; + + sc_guard_run(6); + tc_check(Guard.y, y0, "guard вне комнаты: физика не трогает", __LINE__); +} + +/* check_guard_fallout (seg002:0241): провалившегося ниже комнаты убирают + * совсем — в комнату снизу он не переходит. */ +TC_TEST(char_guard_fallout_removes_him) +{ + sc_room(room_drop2, 1); + sc_kid_at(1, 2, 0); + sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); + + Guard.y = 211; + pop_guard_fallout(); + tc_check(Guard.charid, 0, "guard: убран из комнаты", __LINE__); + tc_check(guardhp_curr, 0, "guard: HP обнулены", __LINE__); +} + +/* ---- окна Char не путают персонажей ---------------------------------- */ + +/* Регресс на класс ошибок, ради которого затевался перенос: физика Кида не + * должна трогать стража, и наоборот. Гоняем оба слота в одной сцене. */ +TC_TEST(char_windows_do_not_leak) +{ + uint8_t gx, gy; + sc_room(room_flat, 1); + sc_kid_at(3, 1, 0); + sc_guard_at(CHARID_2_GUARD, 7, 1, -1, 3); + gx = Guard.x; gy = Guard.y; + + sc_run(SC_R, 10); /* Кид бежит вправо */ + + tc_check(Guard.x, gx, "страж не поехал за Кидом", __LINE__); + tc_check(Guard.y, gy, "страж остался на своей высоте", __LINE__); + tc_check(Guard.charid, CHARID_2_GUARD, "страж не подменён Кидом", __LINE__); + tc_check(Kid.charid, CHARID_0_KID, "Кид не подменён стражем", __LINE__); +} + + +/* ---- follow_guard: страж идёт ЗА КИДОМ вбок (seg002:0340/039E) ------- * + * Своим ходом персонаж слота Guard комнату не меняет, но когда её меняет + * КИД, exit_room может перенести стража следом. Здесь проверяются все + * условия отбора — они и есть механика. Порт зовётся из enter_room, а + * тестируем прямо pop_guard_follow: сцена комнат в наборе одна. */ + +/* Базовый случай: бой, Кид ушёл ВЛЕВО, страж прижат к левому краю (x < 91), + * в комнате 2 своего стража нет → идёт следом, X переносится через шов. */ +TC_TEST(char_follow_guard_left) +{ + tk_guard_state_clear(); + sc_room(room_flat, 3); + sc_kid_at(1, 1, -1); + sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); + Guard.x = 80; /* < 91: у левого края */ + Guard.room = 3; + + tc_check(pop_guard_follow(2, 0), 1, "страж пошёл следом", __LINE__); + tc_check(Guard.room, 2, "страж в новой комнате", __LINE__); + tc_check(Guard.x, 80 + 140, "X перенесён через шов (+140)", __LINE__); + /* follow_guard стирает guards_tile ОБЕИХ комнат — иначе страж + * раздвоится: старую поднимут при возврате, новую при входе. */ + tc_check(tk_gs_tile[3 - 1], 30, "в старой комнате стража нет", __LINE__); + tc_check(tk_gs_tile[2 - 1], 30, "в новой комнате нет второго", __LINE__); +} + +/* Тот же бой, но страж ДАЛЕКО от края (x >= 91) — остаётся. */ +TC_TEST(char_follow_guard_needs_edge) +{ + tk_guard_state_clear(); + sc_room(room_flat, 3); + sc_kid_at(1, 1, -1); + sc_guard_at(CHARID_2_GUARD, 5, 1, -1, 3); + Guard.x = 120; /* >= 91 */ + Guard.room = 3; + + tc_check(pop_guard_follow(2, 0), 0, "далеко от края — остаётся", __LINE__); + tc_check(Guard.room, 3, "комната не менялась", __LINE__); +} + +/* Порог для ухода ВПРАВО — свой (x >= 165). */ +TC_TEST(char_follow_guard_right_threshold) +{ + tk_guard_state_clear(); + sc_room(room_flat, 3); + sc_kid_at(8, 1, 0); + sc_guard_at(CHARID_2_GUARD, 7, 1, 0, 3); + Guard.room = 3; + + Guard.x = 160; /* < 165 — не идёт */ + tc_check(pop_guard_follow(4, 1), 0, "x=160: остаётся", __LINE__); + Guard.x = 170; /* >= 165 — идёт */ + tc_check(pop_guard_follow(4, 1), 1, "x=170: идёт следом", __LINE__); + tc_check(Guard.x, 170 - 140, "X перенесён через шов (−140)", __LINE__); +} + +/* Не бой — не идёт. Меч в ножнах = страж Кида не преследует. */ +TC_TEST(char_follow_guard_needs_sword_drawn) +{ + tk_guard_state_clear(); + sc_room(room_flat, 3); + sc_kid_at(1, 1, -1); + sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); + Guard.x = 80; + Guard.room = 3; + Guard.sword = 0; /* sword_0_sheathed */ + + tc_check(pop_guard_follow(2, 0), 0, "без меча — остаётся", __LINE__); +} + +/* Мёртвый (и умирающий) не идёт никуда. */ +TC_TEST(char_follow_guard_dead_stays) +{ + tk_guard_state_clear(); + sc_room(room_flat, 3); + sc_kid_at(1, 1, -1); + sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); + Guard.x = 80; + Guard.room = 3; + Guard.alive = 0; /* alive >= 0 — умирает/мёртв */ + + tc_check(pop_guard_follow(2, 0), 0, "мёртвый остаётся", __LINE__); +} + +/* Вверх и вниз оригинал не пускает никогда (условия leave всегда истинны). */ +TC_TEST(char_follow_guard_never_vertical) +{ + tk_guard_state_clear(); + sc_room(room_flat, 3); + sc_kid_at(1, 1, -1); + sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); + Guard.x = 80; + Guard.room = 3; + + tc_check(pop_guard_follow(2, 2), 0, "вверх — не идёт", __LINE__); + tc_check(pop_guard_follow(2, 3), 0, "вниз — не идёт", __LINE__); + tc_check(Guard.room, 3, "комната не менялась", __LINE__); +} + +/* В целевой комнате СВОЙ живой страж — перехода нет (иначе их станет два). */ +TC_TEST(char_follow_guard_blocked_by_own_guard) +{ + tk_guard_state_clear(); + sc_room(room_flat, 3); + sc_kid_at(1, 1, -1); + sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); + Guard.x = 80; + Guard.room = 3; + tk_guard_room = 2; /* в комнате 2 есть свой страж */ + + tc_check(pop_guard_follow(2, 0), 0, "чужая комната занята", __LINE__); + tc_check(Guard.room, 3, "остался у себя", __LINE__); + + /* А если там ТРУП (запомнена последовательность) — идти можно. */ + tk_gs_tile[2 - 1] = 11; + tk_gs_seq[2 - 1] = 0x1234; + tc_check(pop_guard_follow(2, 0), 1, "к трупу — можно", __LINE__); +} + +/* Тайлы СОСЕДНИХ комнат: колонка персонажа считается из x и на диапазоне + * x 0..255 лежит в −5..13, то есть регулярно уходит за комнату глубже кромки. + * Там get_tile обязан отдавать настоящий тайл соседа — мнимая стена стоила + * BUG-SWORD-GHOST-1 (страж после follow_guard оказывался в col 12, луч + * видимости упирался в неё, и Кид посреди боя прятал меч, а достать обратно + * уже не мог: draw_sword требует can_guard_see_kid >= 2). */ +static void char_tiles_resolve_across_rooms(void) +{ + static const uint8_t floor30[30] = { + E,E,E,E,E,E,E,E,E,E, + E,E,E,E,E,E,E,E,E,E, + F,F,F,F,F,F,F,F,F,F, + }; + uint8_t i; + /* Комнаты-соседи «уровня»: карта заберёт их fg сама (pop_map_set_edges). */ + for (i = 0; i < 30; i++) { tk_room_fg[4][i] = F; tk_room_fg[6][i] = F; } + tk_room_fg[4][10 + 5] = W; /* сосед слева: ряд 1, col 5 */ + tk_room_fg[6][10 + 2] = W; /* сосед справа: ряд 1, col 2 */ + + sc_room(floor30, 5); + pop_map_set_edges(4, 6, 0, 0); + tc_check(pop_tile_at(-1, 1), F, "col -1 = col9 соседа слева", __LINE__); + tc_check(pop_tile_at(-5, 1), W, "col -5 = col5 соседа слева", __LINE__); + tc_check(pop_tile_at(10, 1), F, "col 10 = col0 соседа справа", __LINE__); + tc_check(pop_tile_at(12, 1), W, "col 12 = col2 соседа справа", __LINE__); + tc_check(pop_tile_at(13, 1), F, "col 13 = col3 соседа справа", __LINE__); + /* Дальше комнаты соседа оригинал ушёл бы ещё через roomlink; у нас + * кэш на одну комнату в каждую сторону, и это край — стена. */ + tc_check(pop_tile_at(20, 1), W, "за комнатой соседа — стена", __LINE__); + tc_check(pop_tile_at(-11, 1), W, "и слева тоже", __LINE__); + + /* Соседа нет (край уровня) — стена в обе стороны, как раньше. */ + pop_map_set_edges(0, 0, 0, 0); + tc_check(pop_tile_at(12, 1), W, "без соседа справа — стена", __LINE__); + tc_check(pop_tile_at(-1, 1), W, "без соседа слева — стена", __LINE__); +} + +/* ---- скелет: падение его не убивает (check_guard_fallout, seg002:252) --- */ + +/* Если под комнатой лежит комната 3, упавший скелет ВОЗРОЖДАЕТСЯ там же — + * это единственный способ «убрать» его с экрана, убить его нельзя. */ +TC_TEST(char_skeleton_reappears_below) +{ + sc_room(room_flat, 1); + sc_kid_at(1, 2, 0); + sc_guard_at(CHARID_4_SKELETON, 5, 1, -1, 3); + Guard.room = 5; + Guard.y = 220; /* ниже нижней границы комнаты (211) */ + tk_link_down = 3; /* под комнатой 5 — комната 3 */ + + pop_guard_fallout(); + + tc_check(Guard.charid, CHARID_4_SKELETON, "скелет остался в слоте", __LINE__); + tc_check(Guard.room, 3, "перенесён в комнату 3", __LINE__); + tc_check(Guard.x, 133, "и на свою позицию", __LINE__); + tc_check(Guard.curr_row, 1, "ряд 1", __LINE__); + tc_check(Guard.alive, -1, "жив", __LINE__); +} + +/* А если снизу НЕ комната 3 — обычная судьба выпавшего: слот освобождается. */ +TC_TEST(char_skeleton_falls_out_elsewhere) +{ + sc_room(room_flat, 1); + sc_kid_at(1, 2, 0); + sc_guard_at(CHARID_4_SKELETON, 5, 1, -1, 3); + Guard.room = 5; + Guard.y = 220; + tk_link_down = 7; /* снизу другая комната */ + + pop_guard_fallout(); + tc_check(Guard.charid, 0, "скелет убран, как обычный выпавший", __LINE__); +} + +void main(void) +{ + sc_init(); + TC_RUN(char_take_hp_goes_to_active); + TC_RUN(char_immortal_covers_kid_only); + TC_RUN(char_delta_hp_applies_per_character); + TC_RUN(char_kid_survives_two_row_fall); + TC_RUN(char_guard_dies_from_two_row_fall); + TC_RUN(char_shadow_survives_two_row_fall); + TC_RUN(char_guard_falls_at_all); + TC_RUN(char_guard_physics_gated_by_x); + TC_RUN(char_guard_fallout_removes_him); + TC_RUN(char_windows_do_not_leak); + TC_RUN(char_follow_guard_left); + TC_RUN(char_follow_guard_needs_edge); + TC_RUN(char_follow_guard_right_threshold); + TC_RUN(char_follow_guard_needs_sword_drawn); + TC_RUN(char_follow_guard_dead_stays); + TC_RUN(char_follow_guard_never_vertical); + TC_RUN(char_follow_guard_blocked_by_own_guard); + TC_RUN(char_tiles_resolve_across_rooms); + TC_RUN(char_skeleton_reappears_below); + TC_RUN(char_skeleton_falls_out_elsewhere); +} diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index b4dcdc7..e5cff0e 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -43,7 +43,8 @@ OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg") # Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты # столбов, ворот, дебриса. См. tile_table.fore_id + анализ pop_background_strategy. -FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100} +FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100} | {106, 107, 108, 109, 110} \ + | {119, 120, 121, 122, 123} # фронт чомпера + его кровь # Пол-оверлей подъёма (floor_left_overlay, draw_floor_overlay seg008): рисуется # рантаймом поверх нижней части Kid при подтягивании на пол у кромки — НЕ @@ -65,6 +66,23 @@ SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left 134, 135, 136, 137, 138, # right 139, 140, 141, 142, 143} # fore +# Кадры ЧОМПЕРА (seg008:638 chomper_fram_bot/top/for + кровь). В уровнях +# чомпер стоит с modif = 0, а chomper_fram1[0] = 3 — значит render_room +# (он анимацию пропускает вовсе, см. render_room.py:611) не кладёт в атлас +# НИ ОДНОГО кадра, и чомпер не виден даже в покое. Поэтому весь набор — +# явно. Низ и верх идут в backtable (ПОД персонажем), фронт — в foretable +# (ПОВЕРХ), поэтому фронтовые id продублированы в FORE_ENV_IDS. +CHOMPER_BOT_IDS = {101, 102, 103, 104, 105} # chomper_fram_bot +CHOMPER_TOP_IDS = {111, 112, 113} # chomper_fram_top (фазы 2..4) +CHOMPER_FORE_IDS = {106, 107, 108, 109, 110} # chomper_fram_for + +# Кровь на сомкнутых челюстях (modif & 0x80): chomper_num + 114 в backtable и +# + 119 в foretable, обе — MONO-блитом цветом 12 (blitters_4Ch_mono_12), то +# есть силуэтом, как пузырёк зелья. Появляется после смерти Кида в чомпере. +CHOMPER_BLOOD_IDS = {114, 115, 116, 117, 118} +CHOMPER_BLOOD_FORE_IDS = {119, 120, 121, 122, 123} +CHOMPER_BLOOD_COLOR = None # ставится ниже, когда известен VGA16_PAL_BASE + # Кадры ОТКРЫТИЯ ворот (portcullis, DOOR_FRAM_SLICE + gate pieces 50/51, # seg008 draw_gate): 50-59 + 67. В уровнях ворота статически ЗАКРЫТЫ # (низкий modif → используются только нижние срезы 50-53); срезы более @@ -105,6 +123,7 @@ VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(42,21,0), (63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета) POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение») +CHOMPER_BLOOD_COLOR = VGA16_PAL_BASE + 12 # тот же mono-цвет 12 (seg008:678) # Основание факела (seg008:489 draw_tile_right: при левом соседе-факеле # рисуется chtab_6 id 146). В статический render_room не попадает. @@ -174,7 +193,7 @@ LEVELDOOR_ENV_IDS = {33, 34, 99, 144} NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149} | LEVELDOOR_ENV_IDS -def load_indexed(chtab, id, opaque=False): +def load_indexed(chtab, id, opaque=False, mono=None): """PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE. Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60). @@ -195,7 +214,11 @@ def load_indexed(chtab, id, opaque=False): if im.mode != "P": die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)") src = im.tobytes() - if opaque: + if mono is not None: + # MONO-блит (blitters_4Ch_mono_12): спрайт красится ОДНИМ цветом из + # 16-цветной палитры экрана — пакуем силуэтом, как пузырёк зелья. + out = bytes((0xFF if v == 0 else mono) for v in src) + elif opaque: out = bytes((pal_base + v) for v in src) else: out = bytes((0xFF if v == 0 else pal_base + v) for v in src) @@ -363,6 +386,11 @@ def main(): | GATE_ANIM_ENV_IDS # + кадры открытия ворот | TORCH_ENV_IDS # + основание факела | STUCK_ENV_IDS # + нажатая кнопка-closer + | CHOMPER_BOT_IDS # + чомпер: низ + | CHOMPER_TOP_IDS # верх + | CHOMPER_FORE_IDS # фронт (поверх Kid) + | CHOMPER_BLOOD_IDS # кровь (mono) + | CHOMPER_BLOOD_FORE_IDS | OPENER_NOFLOOR_ENV_IDS # + opener без пола слева | LEVELDOOR_ENV_IDS) # + дверь уровня (створка/лестница) wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL) @@ -373,7 +401,9 @@ def main(): # ENV фон -> страницы по id>>SHIFT pages = {} for id in env_bg: - w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS) + w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS, + CHOMPER_BLOOD_COLOR if id in CHOMPER_BLOOD_IDS + else None) pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix) max_pg = max(pages) if pages else -1 env_page_names = [] @@ -396,7 +426,10 @@ def main(): print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б") # FORE -> idx=id - fore_entries = {id: load_indexed(R.CHTAB_ENV, id) for id in env_fore} + fore_entries = {id: load_indexed(R.CHTAB_ENV, id, False, + CHOMPER_BLOOD_COLOR + if id in CHOMPER_BLOOD_FORE_IDS else None) + for id in env_fore} cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_fore.atl"), fore_entries) total_bytes += sz print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б") diff --git a/applications/PoP/toolchain/pop_pack_guard.py b/applications/PoP/toolchain/pop_pack_guard.py index 1d03c29..d95dce8 100644 --- a/applications/PoP/toolchain/pop_pack_guard.py +++ b/applications/PoP/toolchain/pop_pack_guard.py @@ -21,6 +21,16 @@ sys.path.insert(0, HERE) import pop_pack_bg as B from pop_pack_kid import transpose_cols +# Набор спрайтов соперника выбирается ТИПОМ УРОВНЯ (load_lev_spr, seg000:1092): +# tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, −1 нет}. +# У обычного стража палитра подменяется из res10.bin по guards_color, у +# остальных наборов curr_guard_color = 0 (seg002:186) — значит цвет берётся +# из ФАЙЛА набора (res750.pal), и подменять его не надо. +SETS = { + "GUARD": ("GUARD", "guard", None), + "SKEL": ("SKEL", "skel", "res750.pal"), +} +SET_NAME = "GUARD" GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD") PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin") OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard") @@ -31,6 +41,32 @@ MASK = (1 << SHIFT) - 1 MAX_ID = 33 # атлас-индексы 0..33 = res751..res784 +def select_set(name): + """Переключить упаковщик на набор (GUARD/SKEL): каталоги и число кадров.""" + global SET_NAME, GUARD_DIR, OUT_DIR, MAX_ID + if name not in SETS: + B.die("неизвестный набор %s (есть: %s)" % (name, ", ".join(SETS))) + SET_NAME = name + src, out, _ = SETS[name] + GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", src) + OUT_DIR = os.path.join(HERE, "..", "poc", "res", out) + # Сколько кадров реально есть в наборе (у скелета их меньше, чем у стража). + n = 0 + while os.path.exists(os.path.join(GUARD_DIR, f"res{751 + n}.png")): + n += 1 + MAX_ID = n - 1 + + +def set_palette(): + """16 цветов набора из его res750.pal (для наборов, где цвет НЕ + подменяется из res10.bin). None — у набора своей палитры нет.""" + fname = SETS[SET_NAME][2] + if fname is None: + return None + b = open(os.path.join(GUARD_DIR, fname), "rb").read() + return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)] + + def load_guard(id): """Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp COLUMN-MAJOR); 0 -> 0xFF (прозрачность). @@ -52,13 +88,86 @@ def load_guard(id): return im.width, im.height, transpose_cols(im.width, im.height, rowmajor) -def guard_palette(): - """16 цветов (r,g,b 0..63) из res10.bin для COLOR.""" +NPAL = 7 # guard_palettes: 7 палитр по 16 цветов (res10.bin) + + +def guard_palette(color=None): + """16 цветов (r,g,b 0..63) из res10.bin для палитры color (1..7). + Индексация как в оригинале (seg003:257): &guard_palettes[0x30*color-0x30]. + color=None — палитра COLOR (та, что запекается в kid.pal по умолчанию).""" + if color is None: + color = COLOR b = open(PAL_BIN, "rb").read() - off = 0x30 * COLOR - 0x30 + off = 0x30 * color - 0x30 return [(b[off + i * 3], b[off + i * 3 + 1], b[off + i * 3 + 2]) for i in range(16)] +def write_pal_header(out_path): + """Таблица ВСЕХ палитр стражей -> заголовок для приложения. + + Оригинал держит спрайты стража в одной палитре, а цвет подменяет на лету: + redraw_screen (seg003:255) зовёт set_chtab_palette(chtab_5_guard, + &guard_palettes[0x30*curr_guard_color - 0x30], 16) ПЕРЕД отрисовкой + комнаты, где curr_guard_color = level.guards_color[room-1] & 0x0F + (enter_guard, seg002:184). У нас спрайты уже лежат в индексах + PAL_BASE + v, поэтому достаточно залить 16 слотов палитры. + + Формат записи — родной для gfx_pal_load: 4 байта (B, G, R, 0).""" + rows = [] + for color in range(1, NPAL + 1): + vals = [] + for (r, g, b) in guard_palette(color): + # res10.bin хранит 6-битные каналы, палитра Sprinter — 8-битные + # (так же делает pop_pack_kid при записи kid.pal). + vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0] + rows.append(" { " + ",".join("0x%02X" % v for v in vals) + " },") + lines = [ + "/* pop_guard_pal.h — палитры стражей (guard_palettes из res10.bin).", + " * СГЕНЕРЕНО toolchain/pop_pack_guard.py — НЕ править вручную.", + " *", + " * Индексация как в оригинале: цвет из level.guards_color[room-1]", + " * (младшие 4 бита), 1..%d -> строка [color-1]; 0 = палитру НЕ менять" % NPAL, + " * (seg003:255 при curr_guard_color == 0 set_chtab_palette не зовёт).", + " * Запись — 4 байта (B, G, R, 0), формат gfx_pal_load. */", + "#ifndef POP_GUARD_PAL_H", + "#define POP_GUARD_PAL_H", + "", + "#include ", + "", + "#define POP_GUARD_PAL_BASE 0x%02X /* слоты Sprinter под chtab_5 */" % PAL_BASE, + "#define POP_GUARD_NPAL %d" % NPAL, + "", + "static const uint8_t pop_guard_pal[POP_GUARD_NPAL][64] = {", + ] + rows + [ + "};", + "", + ] + # Палитра скелета — из ЕГО набора (res750.pal), а не из res10.bin: на + # уровне 3 curr_guard_color = 0, оригинал палитру не подменяет вовсе. + saved = SET_NAME + select_set("SKEL") + skel = set_palette() + select_set(saved) + if skel: + vals = [] + for (r, g, b) in skel: + vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0] + lines += [ + "/* Скелет (SKEL/res750.pal): свой набор — свой цвет. */", + "static const uint8_t pop_skel_pal[64] = {", + " " + ",".join("0x%02X" % v for v in vals) + ",", + "};", + "", + ] + lines += [ + "#endif", + "", + ] + with open(out_path, "w") as f: + f.write("\n".join(lines)) + print(f" pop_guard_pal.h: {NPAL} палитр x 16 цветов = {NPAL*64} Б -> {out_path}") + + def main(): os.makedirs(OUT_DIR, exist_ok=True) pages = {} @@ -77,8 +186,11 @@ def main(): cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {})) total += sz print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б") + write_pal_header(os.path.join(HERE, "..", "roomtest", "pop_guard_pal.h")) print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}") if __name__ == "__main__": + if len(sys.argv) > 1: + select_set(sys.argv[1].upper()) main() diff --git a/docs/TODO.md b/docs/TODO.md index 4e37620..c07f000 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -346,7 +346,7 @@ Quick wins: дошедших make против 9 из 10 без хука. Пользователь на ручной проверке отмечает, что редкие пропуски всё же ощущаются — остаток отложен до финальной полировки, следующий шаг описан там же. - Полный протокол — applications/PoP/roomtest/TASKS.md, KBD-1. + Полный протокол — applications/PoP/roomtest/TASKS_CLOSED.md, KBD-1. Recovery-политика уже терпима к частым overrun'ам (селективный wipe, docs/kbd-games.md), но аккорды «держу →, тапнул ↑» и «держу Shift, тапаю ←» остаются уязвимы. diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 67db660..58ae0ae 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -366,7 +366,7 @@ FIFO SIO — 3 байта, и рассчитывать на «каждый ба опрос луча, а это ~2/3 периода кадра. Проверено: 35 нажатий стрелки с зажатым Shift → 35 дошедших make против 9 из 10 без хука. Остаточные редкие потери возможны (пачка целиком внутри DI-окна одного accel-прохода); -полный протокол и что делать дальше — `applications/PoP/roomtest/TASKS.md`, +полный протокол и что делать дальше — `applications/PoP/roomtest/TASKS_CLOSED.md`, задача KBD-1. **ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/ diff --git a/libbgi/common/gfx_set_idle_hook.c b/libbgi/common/gfx_set_idle_hook.c index bb40f01..20298ca 100644 --- a/libbgi/common/gfx_set_idle_hook.c +++ b/libbgi/common/gfx_set_idle_hook.c @@ -13,7 +13,7 @@ * потерян, байты копятся и один теряется. Лечится вычерпыванием по * опросу (`kbd_raw_poll` из ) — но только ПЛОТНЫМ, раз в * ~0.5 мс, а не пару раз за кадр. Замер: applications/PoP/roomtest/ - * TASKS.md, задача KBD-1. + * TASKS_CLOSED.md, задача KBD-1. * * ТРЕБОВАНИЯ К ХУКУ: он зовётся в тесном цикле, поэтому обязан быть * дешёвым и НЕ рисовать (окно W3 в этот момент принадлежит вызывающему, diff --git a/libc/include/kbd_raw.h b/libc/include/kbd_raw.h index 27aef53..faa8d09 100644 --- a/libc/include/kbd_raw.h +++ b/libc/include/kbd_raw.h @@ -69,7 +69,7 @@ void kbd_raw_sync(void); * (залипание). * * КАК ЕЁ ЗВАТЬ (замеры 2026-08-01, PoP roomtest — см. - * applications/PoP/roomtest/TASKS.md, KBD-1): + * applications/PoP/roomtest/TASKS_CLOSED.md, KBD-1): * - «несколько вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО — * потери те же, что без них: такие вызовы попадают в участки, где * прерывания и так разрешены, и лишь дублируют трамплин; diff --git a/libc/kbd/kbd_raw_poll.c b/libc/kbd/kbd_raw_poll.c index a01c64a..0089dd9 100644 --- a/libc/kbd/kbd_raw_poll.c +++ b/libc/kbd/kbd_raw_poll.c @@ -4,7 +4,7 @@ * PS/2 Set 2, та же битовая карта _kbdraw_down[], тот же FSM префиксов * _kbdraw_pending — просто вызванный из главного цикла приложения. * - * ЗАЧЕМ (разбор 2026-08-01, applications/PoP/roomtest/TASKS.md, KBD-1). + * ЗАЧЕМ (разбор 2026-08-01, applications/PoP/roomtest/TASKS_CLOSED.md, KBD-1). * Приёмный FIFO SIO — 3 байта, и рассчитывать на то, что каждый принятый * байт разбудит нас прерыванием, НЕЛЬЗЯ: * - импульс запроса прерывания клавиатуры живёт лишь 32 такта CPU diff --git a/testkit/host-tests.mk b/testkit/host-tests.mk index c3bb831..768dae6 100644 --- a/testkit/host-tests.mk +++ b/testkit/host-tests.mk @@ -35,11 +35,16 @@ BUILD := build # таблиц анимации, которую на устройстве туда приводит маппинг W0. # EXTRA_INC — заголовки, которых нет ни в testkit, ни в ENGINE_DIR # (например libc/libbgi, когда модуль под тестом их включает). +# DATA_LOC — та же история сверху: набор, чей код перерос 0x8000, обязан +# отодвинуть данные, иначе линкер молча кладёт хвост кода (_HOME, деления +# из libsdcc) ПОВЕРХ _DATA — тест ведёт себя как «зациклился», хотя логика +# ни при чём. Проверку наложения делает check-overlap ниже. CODE_LOC ?= 0x0200 +DATA_LOC ?= 0x8000 EXTRA_INC ?= CFLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size -I$(TESTKIT) \ $(if $(ENGINE_DIR),-I$(ENGINE_DIR)) $(EXTRA_INC) $(EXTRA_CFLAGS) -LDFLAGS := -mz80 --no-std-crt0 --code-loc $(CODE_LOC) --data-loc 0x8000 +LDFLAGS := -mz80 --no-std-crt0 --code-loc $(CODE_LOC) --data-loc $(DATA_LOC) SUITES := $(patsubst t_%.c,%,$(notdir $(wildcard t_*.c))) COMMON := $(BUILD)/crt0_ucsim.rel $(BUILD)/tcheck.rel @@ -60,7 +65,18 @@ all: $(addprefix run-,$(SUITES)) @echo "$(LABEL): все наборы прошли ($(words $(SUITES)))" # Прогон одного набора. Раннер сам разбирает tc_result и возвращает код. +# Наложение кода на данные линкер sdld НЕ считает ошибкой: код просто +# ложится поверх _DATA, и тест виснет/врёт без единого предупреждения. +# Ловим сами — конец _CODE (CODE_LOC + l__CODE) против DATA_LOC. +define CHECK_OVERLAP +@python3 -c "import sys; \ +d={p[1]:int(p[2],16) for p in (l.split() for l in open('$(1)')) if len(p)==3 and p[0]=='DEF'}; \ +end=$(CODE_LOC)+d.get('l__CODE',0); \ +sys.exit(0) if end<=$(DATA_LOC) else sys.exit('%s: КОД НАЕХАЛ НА ДАННЫЕ — _CODE кончается на 0x%04X, а DATA_LOC = 0x%04X.\n Поднять DATA_LOC в Makefile набора (стек в crt0_ucsim — 0xFEF0).' % ('$(1)', end, $(DATA_LOC)))" +endef + run-%: $(BUILD)/%.ihx + $(call CHECK_OVERLAP,$(BUILD)/$*.noi) @python3 $(RUNNER) $(UCSIM) $(BUILD)/$*.ihx $(BUILD)/$*.noi # Удобный псевдоним: `make t_geom` вместо `make run-geom`. diff --git a/tests/kbdpoll/kbdpoll.c b/tests/kbdpoll/kbdpoll.c index 918f86d..1938f5e 100644 --- a/tests/kbdpoll/kbdpoll.c +++ b/tests/kbdpoll/kbdpoll.c @@ -1,7 +1,7 @@ /* * kbdpoll — измерение ПОТОЛКА «плотного опроса» клавиатуры. * - * Вопрос, на который отвечает тест (applications/PoP/roomtest/TASKS.md, + * Вопрос, на который отвечает тест (applications/PoP/roomtest/TASKS_CLOSED.md, * KBD-1): при зажатом Shift часть нажатий расширенных клавиш теряется — * байт не доезжает до порта 0x18, потому что импульс запроса прерывания * пропускается примерно в 44 % случаев и трёхбайтовый FIFO SIO