PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат

Скелет (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 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 22:11:03 +03:00
parent 18d177407b
commit 6673279cef
55 changed files with 3340 additions and 1304 deletions
+3 -2
View File
@@ -66,8 +66,9 @@ Princed для DAT v1.0 (контейнер/индекс/чек-сумма, ко
статусами). статусами).
- `roomtest/`**активная разработка**: уровень 1 целиком (Kid, стражи, - `roomtest/`**активная разработка**: уровень 1 целиком (Kid, стражи,
ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи — ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи —
`roomtest/TASKS.md`, открытые баги — `roomtest/bug_list.md`, закрытые с `roomtest/TASKS_OPEN.md` (закрытые с протоколами —
разбором корней`roomtest/bug_closed.md`. `roomtest/TASKS_CLOSED.md`), открытые баги`roomtest/bug_list.md`,
закрытые с разбором корней — `roomtest/bug_closed.md`.
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`. - `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии. - `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`). - `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
+1 -1
View File
@@ -9,7 +9,7 @@ Kid со всем набором действий, ловушки, ворота,
стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука, стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука,
таймера/HUD, сохранений. таймера/HUD, сохранений.
- Что в работе прямо сейчас — [`roomtest/TASKS.md`](roomtest/TASKS.md). - Что в работе прямо сейчас — [`roomtest/TASKS_OPEN.md`](roomtest/TASKS_OPEN.md).
- Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md). - Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md).
- Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md). - Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
- Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md). - Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
+1 -1
View File
@@ -13,7 +13,7 @@
> **Документ оставлен как СПРАВОЧНИК по модели персонажа** (`char_type`, > **Документ оставлен как СПРАВОЧНИК по модели персонажа** (`char_type`,
> категории `actions_*`, устройство `play_seq`, объём спрайтов) — он нужен > категории `actions_*`, устройство `play_seq`, объём спрайтов) — он нужен
> при портировании остальных акторов (скелет, тень, визирь). Текущие > при портировании остальных акторов (скелет, тень, визирь). Текущие
> задачи — `../roomtest/TASKS.md`. > задачи — `../roomtest/TASKS_OPEN.md`.
Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c` Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c`
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей), (ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
+5 -5
View File
@@ -8,7 +8,7 @@
| Раздел | Что с ним сейчас | | Раздел | Что с ним сейчас |
|--------|------------------| |--------|------------------|
| §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) | | §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) |
| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS.md` (KBD-1) | | §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS_CLOSED.md` (KBD-1) |
| §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) | | §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) |
| §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) | | §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) |
| §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой | | §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой |
@@ -18,7 +18,7 @@
| §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` | | §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` |
**Где смотреть текущее состояние, а не план:** `../roomtest/README.md` **Где смотреть текущее состояние, а не план:** `../roomtest/README.md`
(что играется), `../roomtest/TASKS.md` (что в работе), `levels_plan.md` (что играется), `../roomtest/TASKS_OPEN.md` (что в работе), `levels_plan.md`
(следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам). (следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам).
--- ---
@@ -409,7 +409,7 @@ memory/png_strip_padding_tradeoff.
осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
комнатами во все четыре стороны. Осталось: **старт по данным уровня** комнатами во все четыре стороны. Осталось: **старт по данным уровня**
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в (`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
`../roomtest/TASKS.md`. `../roomtest/TASKS_OPEN.md`.
**Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через **Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
`LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой `LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
@@ -456,13 +456,13 @@ tiny/small.
1. ~~**Held-state клавиатуры** (§2)~~ — **закрыт** (`<kbd_raw.h>`). Открытый 1. ~~**Held-state клавиатуры** (§2)~~ — **закрыт** (`<kbd_raw.h>`). Открытый
остаток — не «есть ли held-state», а потеря байт при аккордах остаток — не «есть ли held-state», а потеря байт при аккордах
Shift+стрелка: `../roomtest/TASKS.md`, KBD-1. Shift+стрелка: `../roomtest/TASKS_CLOSED.md`, KBD-1.
2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался: 2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
спрайтовый движок для персонажей не используется, поэтому лимит спрайтовый движок для персонажей не используется, поэтому лимит
«~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и «~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
(~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()` (~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). `../roomtest/bug_list.md` (T-1/T-2).
3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28 3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5; атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
+1 -1
View File
@@ -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` рядом) | | [`../roomtest/bug_list.md`](../roomtest/bug_list.md) | Открытые баги roomtest (закрытые — в `bug_closed.md` рядом) |
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | | [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки | | [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
+1 -1
View File
@@ -13,7 +13,7 @@
> **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка > **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка
> объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные > объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные
> ссылки на механику SDLPoP) продолжают экономить время при отладке. > ссылки на механику SDLPoP) продолжают экономить время при отладке.
> Текущие задачи — `../roomtest/TASKS.md`. > Текущие задачи — `../roomtest/TASKS_OPEN.md`.
Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт
«с чистого листа» (пустой контекст). Всё сверено с «с чистого листа» (пустой контекст). Всё сверено с
+3 -3
View File
@@ -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`, `bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`, `sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
`libc_one_function_per_module`. `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` уходят на **Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на
него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже
тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`). тем же приёмом закрыты спрайты персонажей (`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`/`_bgi_end`) ставит САМА libbgi — вызывать
её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
@@ -500,7 +500,7 @@ memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже
вместо скобки на блит; нужен публичный batch-API в libbgi. вместо скобки на блит; нужен публичный batch-API в libbgi.
**Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`, **Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`,
длинная серия задержит кадровое прерывание — а по разбору KBD-1 длинная серия задержит кадровое прерывание — а по разбору KBD-1
(`../roomtest/TASKS.md`) длинные DI-окна и есть причина потери байт (`../roomtest/TASKS_CLOSED.md`) длинные DI-окна и есть причина потери байт
клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую. клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую.
2. **Решётка ворот одним спрайтом**`draw_gate_back` рисует бары по одному 2. **Решётка ворот одним спрайтом**`draw_gate_back` рисует бары по одному
(до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним (до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
+2 -2
View File
@@ -21,7 +21,7 @@ up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`,
функции формирования имени. функции формирования имени.
- **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`, - **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`,
`pop_level_start_pos()`, `pop_level_start_dir()` — реализованы и пока `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_level_guard()` (порт `enter_guard`),
сохранение состояния между комнатами (`pop_guard_state_save`). сохранение состояния между комнатами (`pop_guard_state_save`).
- **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py` - **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py`
@@ -70,7 +70,7 @@ up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`,
> **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 → > **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 →
> Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие > Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие
> колонны рисуются) → уровень 3. Разбор что именно сделано и что по > колонны рисуются) → уровень 3. Разбор что именно сделано и что по
> уровню 2 осталось — `../roomtest/TASKS.md`, запись **L2**. > уровню 2 осталось — `../roomtest/TASKS_CLOSED.md`, запись **L2**.
> >
> Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется: > Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется:
> **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина > **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина
+1 -1
View File
@@ -14,7 +14,7 @@
> **Зачем документ остаётся.** Полная straddle-модель понадобится для: > **Зачем документ остаётся.** Полная straddle-модель понадобится для:
> (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4), > (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4),
> (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у > (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у
> шва (S4). Брать из `../roomtest/TASKS.md`, когда дойдёт очередь. > шва (S4). Брать из `../roomtest/TASKS_OPEN.md`, когда дойдёт очередь.
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате, Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP. пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10 -4
View File
@@ -11,10 +11,16 @@
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP = по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
по умолчанию баг у нас. по умолчанию баг у нас.
**Что в работе сейчас — `TASKS.md`** (доска задач: приоритеты, критерии **Что в работе сейчас — `TASKS_OPEN.md`** (доска задач: приоритеты, критерии
готовности). Баги — `bug_list.md` (только ОТКРЫТЫЕ) и `bug_closed.md` готовности); сделанное с протоколами замеров — `TASKS_CLOSED.md`. Баги —
(закрытые + разбор корней: перед заведением нового бага грепни там по `bug_list.md` (только ОТКРЫТЫЕ) и `bug_closed.md` (закрытые + разбор корней:
симптому). План следующих уровней — `../docs/levels_plan.md`. перед заведением нового бага грепни там по симптому); сырые формулировки
пользователя с прогонов — `bugs_level1.md` / `bugs_level2.md`. План
следующих уровней — `../docs/levels_plan.md`.
Правило разнесения: как только задача/баг закрыт — запись целиком переезжает
в `TASKS_CLOSED.md` / `bug_closed.md`, а в открытом файле остаётся ссылка.
Открытые файлы обязаны читаться целиком за раз.
## Сборка и запуск ## Сборка и запуск
+11 -2
View File
@@ -26,7 +26,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# библиотек) — на roomtest это МИНУТЫ на банк, потому что модули крупные. # библиотек) — на roomtest это МИНУТЫ на банк, потому что модули крупные.
# Дефолт SDCC — 3000, и для разработки его достаточно: разница в размере # Дефолт SDCC — 3000, и для разработки его достаточно: разница в размере
# единицы процента, а сборка кратно быстрее. Замер (2026-08-05) — в шапке # единицы процента, а сборка кратно быстрее. Замер (2026-08-05) — в шапке
# TASKS.md, запись BUILD-FAST. # TASKS_CLOSED.md, запись BUILD-FAST.
# #
# make — быстрая сборка (ALLOCS=3000) # make — быстрая сборка (ALLOCS=3000)
# make ALLOCS=100000 — как раньше: минимальный код, для замеров размера # make ALLOCS=100000 — как раньше: минимальный код, для замеров размера
@@ -67,8 +67,13 @@ KID_BIN := $(KID_DIR)/kid_data.bin
GUARD_DIR := $(CURDIR)/../poc/res/guard GUARD_DIR := $(CURDIR)/../poc/res/guard
GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \ GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \
$(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl $(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl
# Скелет — ОТДЕЛЬНЫЙ набор спрайтов (SKEL.DAT в оригинале): его грузит
# уровень 3 (tbl_guard_type == 2), кадров меньше (28 против 34).
SKEL_DIR := $(CURDIR)/../poc/res/skel
SKEL_DATA := $(SKEL_DIR)/g0.atl $(SKEL_DIR)/g1.atl $(SKEL_DIR)/g2.atl \
$(SKEL_DIR)/g3.atl
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(LVL_DATA) EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) $(LVL_DATA)
include $(PROJ_ROOT)/app.mk include $(PROJ_ROOT)/app.mk
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS), # Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
@@ -83,6 +88,7 @@ hdd: $(EXAMPLE).exe
$(foreach f,$(BG_DATA),BG:$(f)) \ $(foreach f,$(BG_DATA),BG:$(f)) \
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \
$(foreach f,$(SKEL_DATA),SKEL:$(f)) \
$(foreach f,$(LVL_DATA),LEVELS:$(f)) $(foreach f,$(LVL_DATA),LEVELS:$(f))
TC := $(PROJ_ROOT)/applications/PoP/toolchain TC := $(PROJ_ROOT)/applications/PoP/toolchain
@@ -91,6 +97,9 @@ $(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
$(GUARD_DATA): $(TC)/pop_pack_guard.py $(GUARD_DATA): $(TC)/pop_pack_guard.py
cd $(TC) && python3 pop_pack_guard.py cd $(TC) && python3 pop_pack_guard.py
$(SKEL_DATA): $(TC)/pop_pack_guard.py
cd $(TC) && python3 pop_pack_guard.py SKEL
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
cd $(TC) && python3 pop_pack_kid.py cd $(TC) && python3 pop_pack_kid.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
+13 -8
View File
@@ -39,14 +39,19 @@ ESC выход.
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid - `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
(оба генерируются скриптами `../toolchain/`). (оба генерируются скриптами `../toolchain/`).
## Статус (2026-08-01) ## Статус (2026-08-07)
Играется весь уровень 1: комнаты и переходы между ними, Kid (анимация, **Уровни 1 и 2 приняты** (пользователь прошёл все комнаты обоих, крупных
управление, коллизия/падение, зацеп/подтягивание/спуск, окклюзия), кнопки и багов нет): комнаты и переходы, Kid (анимация, управление, коллизия/падение,
ворота, пики, проваливающиеся полы, дверь уровня, меч и бой, стражи с ИИ, зацеп/подтягивание/спуск, окклюзия), кнопки и ворота, пики, проваливающиеся
HP и зелья. полы, дверь уровня и переход на следующий уровень, меч и бой, стражи с ИИ,
HP и зелья, чекпойнт уровня 3.
Не сделано: выход в следующий уровень (дверь открывается, но войти в неё Уровень 3 играется, но не принят: нет **чомперов** и **скелета**. Не сделано
нельзя), звук, таймер/HUD времени, сохранения. **Что берём в работу вовсе: звук, таймер/HUD времени, меню и сохранения.
сейчас — [`TASKS.md`](TASKS.md)**; план следующих уровней —
**Что берём в работу сейчас — [`TASKS_OPEN.md`](TASKS_OPEN.md)** (сделанное с
протоколами замеров — [`TASKS_CLOSED.md`](TASKS_CLOSED.md)); открытые баги —
[`bug_list.md`](bug_list.md), закрытые с разбором корней —
[`bug_closed.md`](bug_closed.md); план следующих уровней —
[`../docs/levels_plan.md`](../docs/levels_plan.md). [`../docs/levels_plan.md`](../docs/levels_plan.md).
File diff suppressed because it is too large Load Diff
+487
View File
@@ -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 — делаем сейчас
### <a id="guard-phys"></a>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_room1] >= 30 || // в новой комнате
guards_seq_hi[kid_room1] != 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 на
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
живой страж — переход не происходит.
### <a id="l3-chomp"></a>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)); смерть Кида в сомкнутых челюстях.
### <a id="l3-skel"></a>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`, и оба изменения живут в одном упаковщике.
### <a id="l3-color"></a>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)).
### <a id="draw-char"></a>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` зелёные.
### <a id="l3-pass"></a>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).
### <a id="rooms-graph"></a>Справка: связность комнат уровней 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 — берётся в любой момент
### <a id="l1-speed"></a>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, не «на глаз».
### <a id="tune-1"></a>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.
### <a id="mem-next"></a>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).
+287 -1
View File
@@ -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`, подстановка тайла нажатой разбор КОРНЯ (odd-pixel арифметика `char_x`, подстановка тайла нажатой
@@ -11,6 +13,127 @@
--- ---
<a id="bug-guard-color-1"></a>
## 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[room1] & 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).
---
<a id="bug-sword-ghost-1"></a>
## 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** ## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ЗАКРЫТ 2026-08-05**
**Проверено в игре (пользователь):** «зацеп работает». Физика была верна с **Проверено в игре (пользователь):** «зацеп работает». Физика была верна с
@@ -404,6 +527,7 @@ K3; K2b просто запускает run-jump». Полировкой это
--- ---
<a id="bug-fall-sword-1"></a>
## BUG-FALL-SWORD-1. Отход с мечом в провал: не та последовательность падения — **ЗАКРЫТ 2026-08-04** ## BUG-FALL-SWORD-1. Отход с мечом в провал: не та последовательность падения — **ЗАКРЫТ 2026-08-04**
**Симптом (приёмка уровня 2, комната 4).** Кид с вынутым мечом отступает **Симптом (приёмка уровня 2, комната 4).** Кид с вынутым мечом отступает
@@ -550,6 +674,7 @@ pop_gframe (кадр СТРАЖА, image 17): dx=-1 weight_x=8
--- ---
<a id="bug-land-sword-1"></a>
## BUG-LAND-SWORD-1. Кид навсегда застревает в приседе после падения с мечом — **ЗАКРЫТ 2026-08-04** ## BUG-LAND-SWORD-1. Кид навсегда застревает в приседе после падения с мечом — **ЗАКРЫТ 2026-08-04**
**Симптом (приёмка уровня 2).** Страж ударил Кида, Кид потерял HP, **Симптом (приёмка уровня 2).** Страж ударил Кида, Кид потерял HP,
@@ -1521,3 +1646,164 @@ dir_56_none`. То есть стражи — такая же часть дан
`../docs/room_model_plan.md`): держать `Kid.room` отдельно от `drawn_room` и `../docs/room_model_plan.md`): держать `Kid.room` отдельно от `drawn_room` и
рисовать со смещением ±140, как `xpos_in_drawn_room` (seg004:0405). Пока рисовать со смещением ±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).
<a id="ручная-перепроверка-2026-08-03"></a>
## Ручная перепроверка фиксов (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). Не всплыло.
<a id="обход-всех-24-комнат-уровня-1"></a>
## Обход всех 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_y3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
| 13, 18, 24 | недостижимы в игре | свойство данных уровня, разбор — «НЕ БАГИ» выше |
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
## Сырые наблюдения (прогон 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, который остаётся открытым (ждёт сценария).
---
<a id="bug-guard-splash-1"></a>
## 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) —
отдельной работы не требует.
+93 -185
View File
@@ -4,82 +4,84 @@
переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг, переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
грепни там по симптому. грепни там по симптому.
Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта: Приоритеты работ — в [`TASKS_OPEN.md`](TASKS_OPEN.md) (закрытые задачи с
механику сверять с `../SDLPoP/src/` ДО кодинга. протоколами — [`TASKS_CLOSED.md`](TASKS_CLOSED.md)), а не здесь. Правило
проекта: механику сверять с `../SDLPoP/src/` ДО кодинга.
Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1). Одиннадцать **Ревизия списка: 2026-08-07 — прогон ВСЕХ комнат уровней 1 и 2**
наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены (пользователь). Крупных багов нет. Поэтому в [`bug_closed.md`](bug_closed.md)
в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень уехали разом: чек-листы ручной перепроверки фиксов (2026-08-03, обе волны),
стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень), таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений того же прогона —
BUG-DEATH-1 (смерть от меча), BUG-GATE-ANIM-1 (анимация ворот), BUG-COLL-1 все они закрыты этим проходом. С прогона пришли **три новые записи**, все по
(полный порт `check_collisions`), BUG-STANDUP-1 (лишний guard в уровню 2 (сырые формулировки — [`bugs_level2.md`](bugs_level2.md)).
`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 — однократное наблюдение прохода сквозь закрывшуюся решётку,
которое ЖДЁТ надёжного сценария.
| ID | что | тип | статус | | ID | что | тип | статус |
|----|-----|-----|--------| |----|-----|-----|--------|
| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу | | [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1) | `+`/`` в бою с вынутым мечом → Кид теряет управление | Major (чит) | открыт |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария** (BUG-GATEMOD-1, из-за которого решётка стартовала не в том состоянии, закрыт — перепроверить) | | [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-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт | | [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается |
--- ---
<a id="уровень-2"></a> <a id="уровень-2"></a>
# Уровень 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):** наблюдения СНАЧАЛА копятся Ниже — то, что осталось открытым после прогона 2026-08-07.
здесь целиком — фиксы берём, когда уровень пройден до конца. Причина:
косметика отрисовки почти всегда сводится к общему корню (ср. волну
BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит
чинить один и тот же корень трижды.
Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие <a id="bug-cheat-fight-1"></a>
кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass), ## BUG-CHEAT-FIGHT-1. Чит `+`/`−` в бою: Кид остаётся в режиме боя и теряет управление
запись **L2-PASS**: по ней видно, «механика не сработала» это или
«так и задумано».
Перед заведением записи — грепнуть [`bug_closed.md`](bug_closed.md) по **Наблюдение (пользователь, 2026-08-07).** Если нажать `+`/`` (ROOMNAV,
симптому: почти вся косметика окклюзии уровня 1 уже разбиралась переход по комнатам) в момент, когда Кид вытащил меч для битвы, — в новой
(BUG-CEIL-1/2/3, BUG-OCCL-1, BUG-DOOR-CLIP, BUG-DRAWORDER-1), и уровень 2 комнате Кид не управляется: нажатия стрелок игнорируются.
вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9)
и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый.
<a id="уровень-3"></a> **Корень (прочитан по коду, сверен с seg005).** Чит ROOMNAV
# Уровень 3 — баги с приёмки (заведено 2026-08-05) (`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);
}
```
| наблюдение | что это на самом деле | Стража в новой комнате нет (`can_guard_see_kid` = 0), кадр после
|------------|------------------------| `kid_init(SEQ_STAND)` — обычная стойка, а не 171, поэтому не срабатывает ни
| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта | `swordfight()`, ни ветка «убрать меч»: `control_with_sword` каждый кадр
| к.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 | не делает НИЧЕГО, и ввод не доходит до движения. Заклинивание вечное.
Остальные два были настоящими багами и **закрыты в тот же день** — разбор Это баг **нашего чита**, а не порта: в оригинале телепорта между комнатами
корней в [`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`, так что проверить сценарием, а не менять вслепую.
<a id="bug-spike-1"></a> <a id="bug-spike-1"></a>
## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ ## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
@@ -124,28 +126,13 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б
`animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked` `animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked`
сверены с seg006/seg007 построчно и совпадают дословно. сверены с 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) и [наш](bugscreens/l2-r6-spikes-ours.png) и
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.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).** Чистый 2026-08-05, MAME, watchpoint на `room_modif[13]` комнаты 6).** Чистый
пробег по УБРАННЫМ пикам убивает штатно: пробег по УБРАННЫМ пикам убивает штатно:
@@ -184,45 +171,21 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б
--- ---
<a id="уровень-3"></a>
# Уровень 3 — не баги, а неначатые задачи
<a id="ручная-перепроверка-2026-08-03"></a> Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два
# Ручная перепроверка фиксов (2026-08-03) были настоящими багами и **закрыты в тот же день** (разбор — в
[`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 пересобран, Приёмки уровня 3 (полного обхода комнат) ещё не было — она осмысленна только
`make size-check` без роста). после L3-CHOMP/L3-SKEL.
| # | что проверить | ожидаемо |
|---|---------------|----------|
| 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).
--- ---
@@ -231,11 +194,14 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б
<a id="bug-gate-pass-1"></a> <a id="bug-gate-pass-1"></a>
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ ## 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,1). и упал на (1,1).
@@ -266,7 +232,8 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б
3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать. 3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.
Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2** Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
продолжить), затем чтение `_Kid` из отладчика MAME. продолжить), затем чтение `_Kid` из отладчика MAME; подгонка позы по пикселю —
читы `[`/`]`.
**Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке, **Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
@@ -275,6 +242,8 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может `can_bump_into_gate()`: если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени. перестать считаться препятствием раньше времени.
---
# Оптимизация отрисовки (записано 2026-07-29) # Оптимизация отрисовки (записано 2026-07-29)
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
@@ -328,51 +297,8 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б
heal'а нет, стирать пики нечем, редрой не нужен. heal'а нет, стирать пики нечем, редрой не нужен.
**Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно **Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно
там, где тапают Shift+стрелку (см. остаток KBD-1 в [`TASKS.md`](TASKS.md)). там, где тапают Shift+стрелку (остаток KBD-1 отложен, см.
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#kbd-1)).
---
# Обход всех 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_y3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
| 16 | | |
| 17 | | |
| 18 | недостижима в игре | |
| 19 | | |
| 20 | | |
| 21 | | |
| 22 | | |
| 23 | | |
| 24 | недостижима в игре | |
--- ---
@@ -382,32 +308,14 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в (эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
сторону = разворот (как в оригинале safe_step), движение со второго. сторону = разворот (как в оригинале safe_step), движение со второго.
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle), - Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
**S** — выдать меч. **S** — выдать меч, **Shift+L** — следующий уровень, **`[`/`]`** — сдвиг
Кида на пиксель по X.
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит - Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos). Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и - **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в `S`. Все они со временем съедутся в общий блок читов, разрешаемый в
настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`). настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
Известный баг этого чита — [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1).
- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство
--- данных уровня (разбор — «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md));
приоритет багов в них низкий. Аналогично 23/24 на уровне 3.
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
## Сырые наблюдения (прогон 2026-08-03) → корень
Формулировки с прогона, как записаны. Закрытые корни — в
[`bug_closed.md`](bug_closed.md).
| # | наблюдение (кратко) | корень | статус |
|---|---------------------|--------|--------|
| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт |
| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт |
| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт |
| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт |
| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт |
| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт |
| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) |
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) |
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт |
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт |
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт |
+10
View File
@@ -0,0 +1,10 @@
Баги/недоработки на уровне 2
- комната 4 - бой со стражем - страж другого цвета/другого цвета у него HP (сравнение с SDLPoP)
- комната 4 - бой со стражем - при нанесении урона стражу должен рисоваться сплеш (типа звезды) - что бы игрок
видел что удар дошел до цели без перевода внимания на HP (
- баг нашего cheats - если мы нажимаем +/- (переход по комнатам) в момент когда Кид вытащил меч для битвы -
Кид теряет управление (он переключается в режим боя но в новой комнате нету стража - в результате нажатия
кнопок движения игнорятся)
+97 -23
View File
@@ -21,11 +21,13 @@
#include "pop_map.h" #include "pop_map.h"
#include "pop_geom.h" #include "pop_geom.h"
#include "pop_trob.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 TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */ #define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
#define TILE_SPIKE 2
#define TILE_GATE 4 #define TILE_GATE 4
#define TILE_DOORTOP_FLOOR 7 #define TILE_DOORTOP_FLOOR 7
#define TILE_LOOSE 11 #define TILE_LOOSE 11
@@ -33,6 +35,10 @@
#define TILE_CHOMPER 18 #define TILE_CHOMPER 18
#define TILE_WALL 20 #define TILE_WALL 20
/* actions (types.h:334) — нужны ветке «идти ли за упавшим Кидом». */
#define ACT_HANGCLIMB 2
#define ACT_HANGSTRAIGHT 6
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и /* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его * «половинки» 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)) { if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
can_guard_see_kid = 1; can_guard_see_kid = 1;
} else if (tile == TILE_GATE && } 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; /* ворота подняты не до конца */ can_guard_see_kid = 1; /* ворота подняты не до конца */
} }
left_pos += TILE_SIZEX; 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..) --------------------------- */ /* ---- Синтетический ввод (seg002:0706..) --------------------------- */
static void move_0_nothing(void) static void move_0_nothing(void)
{ {
@@ -281,6 +337,35 @@ static void autocontrol_guard_kid_far(void)
move_2_backward(); 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). */ /* autocontrol_guard_active (seg002:737). */
static void autocontrol_guard_active(void) static void autocontrol_guard_active(void)
{ {
@@ -293,9 +378,11 @@ static void autocontrol_guard_active(void)
return; return;
if (can_guard_see_kid == 0) { if (can_guard_see_kid == 0) {
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе /* seg002:740: Кид пропал из виду. Если его СПИХНУЛИ (droppedout) —
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */ * решаем, идти ли за ним вниз; иначе просто отступаем. Скелет не
if (Char.charid != CHARID_4_SKELETON) move_down_back(); * отступает никогда (у него нет этой ветки в оригинале). */
if (pop_droppedout) guard_follows_kid_down();
else if (Char.charid != CHARID_4_SKELETON) move_down_back();
return; return;
} }
@@ -339,6 +426,9 @@ static void autocontrol_opponent(void)
if (justblocked) --justblocked; if (justblocked) --justblocked;
if (kid_sword_strike) --kid_sword_strike; if (kid_sword_strike) --kid_sword_strike;
if (guard_refrac) --guard_refrac; 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(); if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
else autocontrol_guard_active(); else autocontrol_guard_active();
} }
@@ -355,22 +445,6 @@ static void autocontrol_opponent(void)
static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] = static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
{ 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0}; { 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): применить попадание к АКТИВНОМУ персонажу. /* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр * Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
* «получил удар». * «получил удар».
@@ -392,9 +466,9 @@ static void hurt_by_sword(void)
return; return;
} }
if (Char.sword != SWORD_2_DRAWN) { if (Char.sword != SWORD_2_DRAWN) {
take_hp(100); pop_take_hp(100);
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); 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 кончились */ pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
} else { } else {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD); pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
+45 -1
View File
@@ -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). Появятся падающие/висящие стражи — тогда и общий * (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
* порт redraw_at_char2, а не догадки сейчас. */ * порт redraw_at_char2, а не догадки сейчас. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h, 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; int8_t r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */ fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir, sword); 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); 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 (r = fp_rT; r <= fp_rB; r++)
for (c = fp_cL; c <= fp_cR; c++) for (c = fp_cL; c <= fp_cR; c++)
fore_tile(r, c); fore_tile(r, c);
+7 -1
View File
@@ -79,8 +79,14 @@ void pop_fore_set_clip(int x, int y, int w, int h) __banked;
/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому /* __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, 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/полоса потолка): он выходит за * (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
+3
View File
@@ -17,6 +17,7 @@
*/ */
#include <stdint.h> #include <stdint.h>
#include <kbd_raw.h> #include <kbd_raw.h>
#include "pop_state.h"
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */ #include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_map.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.charid < CHARID_2_GUARD) {
if (Char.frame == FRAME_171_STAND_WITH_SWORD) { if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
Char.sword = SWORD_0_SHEATHED; Char.sword = SWORD_0_SHEATHED;
+144 -10
View File
@@ -19,7 +19,33 @@
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */ #include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
#include "pop_level.h" /* тип соперника уровня (набор спрайтов) */
#include "kid_data.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 ---------- */ /* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
#define GUARD_PAGES 5 #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 uint16_t sw_l[2], sh_l[2];
static uint8_t svalid[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 /* Ширина спрайта последнего кадра стража — нужна set_char_collision
* (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам: * (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; } 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 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\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
"GUARD\\g3.atl", "GUARD\\g4.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; 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; if (atlas_load(&gp[i], names[i]) != 0) return -1;
}
g_ok = 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; return 0;
} }
@@ -77,7 +134,8 @@ void pop_guard_heal(void) __banked
uint8_t p = gfx_get_draw_page() & 1; 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 (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]); 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 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)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) { if (w && h) {
int bx = obj_x; 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); /* спрайты нарисованы влево */ flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
if (flip) bx -= (int)w; if (flip) bx -= (int)w;
top = obj_y - (int)h + 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; p = gfx_get_draw_page() & 1;
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0); gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip,
(uint8_t)skip, (uint8_t)rows);
else else
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0); gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, skip, rows);
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1; /* 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, см. выше */ 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, /* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник * seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
@@ -157,7 +289,9 @@ void pop_guard_draw(void) __banked
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний * (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
* столб. */ * столб. */
if (w && h) 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) --- * /* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
+127 -3
View File
@@ -11,11 +11,13 @@
#include "pop_level.h" #include "pop_level.h"
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */ #include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
#include "pop_map.h" /* hitp_* — HP Кида */ #include "pop_map.h" /* hitp_* — HP Кида */
#include "pop_state.h" /* pop_immortal — чит бессмертия */
#include "kid_data.h" #include "kid_data.h"
pop_char_t Guard; pop_char_t Guard;
uint8_t guardhp_curr, guardhp_max; uint8_t guardhp_curr, guardhp_max;
int8_t guardhp_delta; int8_t guardhp_delta;
uint8_t pop_guard_hurt;
int8_t can_guard_see_kid; int8_t can_guard_see_kid;
int8_t is_guard_notice; int8_t is_guard_notice;
uint8_t guard_skill; uint8_t guard_skill;
@@ -66,6 +68,20 @@ void pop_guard_kill(void)
Guard.alive = 0; 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 и обнулить /* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */ * её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
void pop_do_delta_hp(void) void pop_do_delta_hp(void)
@@ -76,7 +92,10 @@ void pop_do_delta_hp(void)
if (hp < 0) hp = 0; if (hp < 0) hp = 0;
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max; if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
hitp_curr = (uint8_t)hp; 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; hitp_delta = 0;
} }
if (guardhp_delta) { if (guardhp_delta) {
@@ -84,10 +103,52 @@ void pop_do_delta_hp(void)
if (hp < 0) hp = 0; if (hp < 0) hp = 0;
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max; if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
guardhp_curr = (uint8_t)hp; guardhp_curr = (uint8_t)hp;
if (guardhp_delta < 0) pop_guard_hurt = 1; /* -> брызги поверх стража */
guardhp_delta = 0; 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): таблица у /* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame 149, где * стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535). */ * add_frame = 70 для кадров 102..106 (seg006:535). */
@@ -114,6 +175,60 @@ void pop_guard_leave(void)
(uint8_t)(Guard.alive >= 0)); (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 персонажа — /* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа —
* floor((xpos 7 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */ * floor((xpos 7 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */
static int8_t guard_col_from_x(uint8_t xpos) 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_type(pop_current_level) < 0) return;
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) 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 считается из /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
* колонки тайла, а не берётся из файла. */ * колонки тайла, а не берётся из файла. */
Guard.room = room; 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.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Guard.curr_col = (int8_t)(tile % 10); Guard.curr_col = (int8_t)(tile % 10);
Guard.direction = dir; Guard.direction = dir;
Guard.charid = CHARID_2_GUARD; /* seg002:196: на уровне со скелетами слот поднимает СКЕЛЕТА, и он
Guard.sword = 0; /* sword_0_sheathed */ * встаёт сразу активным с вынутым мечом (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.alive = -1;
Guard.action = 1; /* actions_1_run_jump */ Guard.action = 1; /* actions_1_run_jump */
Guard.fall_x = Guard.fall_y = 0; Guard.fall_x = Guard.fall_y = 0;
+43
View File
@@ -33,7 +33,9 @@
#define SEQ_66_STRIKE_AFTER_PARRY 66 #define SEQ_66_STRIKE_AFTER_PARRY 66
#define SEQ_71_DYING 71 #define SEQ_71_DYING 71
#define SEQ_75_STRIKE 75 /* удар (Кид) */ #define SEQ_75_STRIKE 75 /* удар (Кид) */
#define SEQ_63_GUARD_ACTIVE_AFTER_FALL 63 /* стойка АКТИВНОГО (скелет) */
#define SEQ_77_GUARD_STAND_INACTIVE 77 #define SEQ_77_GUARD_STAND_INACTIVE 77
#define SEQ_88_SKEL_WAKE_UP 88 /* скелет встаёт (спецсобытие) */
#define SEQ_80_STAND_FLIPPED 80 #define SEQ_80_STAND_FLIPPED 80
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */ #define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
#define SEQ_87_GUARD_BECOME_INACT 87 #define SEQ_87_GUARD_BECOME_INACT 87
@@ -76,6 +78,23 @@ extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты
extern uint8_t guardhp_curr, guardhp_max; extern uint8_t guardhp_curr, guardhp_max;
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */ 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): /* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
* 0 не видит, 1 видит, но не пойдёт (между ними loose/чомпер/дыра), * 0 не видит, 1 видит, но не пойдёт (между ними loose/чомпер/дыра),
* 2 видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в * 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). */ * check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
void pop_check_can_guard_see_kid(void) __banked; 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. */ * кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
void pop_check_sword_hurting(void) __banked; void pop_check_sword_hurting(void) __banked;
@@ -119,6 +144,11 @@ void pop_check_sword_hurt(void) __banked;
extern kframe pop_gframe; extern kframe pop_gframe;
void pop_guard_load_frame(void); 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); uint8_t pop_guard_present(void);
@@ -135,6 +165,13 @@ void pop_guard_enter(uint8_t room);
* enter_room, пока Guard ещё держит прежнего. */ * enter_room, пока Guard ещё держит прежнего. */
void pop_guard_leave(void); 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, /* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
* как kid_draw) и стереть прошлый (heal). Реализация pop_gdraw.c * как kid_draw) и стереть прошлый (heal). Реализация pop_gdraw.c
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */ * (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
@@ -144,6 +181,12 @@ void pop_hp_draw(void) __banked;
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */ /* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
void pop_hp_invalidate(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_heal(void) __banked;
void pop_guard_draw(void) __banked; void pop_guard_draw(void) __banked;
+31
View File
@@ -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 <stdint.h>
#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
+52 -42
View File
@@ -71,7 +71,8 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
if (!sword_ok) return 0; if (!sword_ok) return 0;
if (!((ch->frame >= 229 && ch->frame < 238) || if (!((ch->frame >= 229 && ch->frame < 238) ||
ch->sword != 0 || 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; return 0;
sf = (uint8_t)(fr->sword & 0x3F); sf = (uint8_t)(fr->sword & 0x3F);
if (sf == 0 || sf >= KID_NSWORD) return 0; 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]); bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1; top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
if (flip) bx -= (int)w; if (flip) bx -= (int)w;
/* Поле комнаты — 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); gfx_set_bank(GFX_BANK_SPRITE);
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h: /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */ * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0); gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip,
(uint8_t)skip, (uint8_t)rows);
else else
gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0); gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, skip, rows);
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
*rx = bx; *ry = top; *rw = w; *rh = h; *rx = bx; *ry = top; *rw = w; *rh = (uint16_t)rows;
}
return 1; return 1;
} }
@@ -214,34 +225,18 @@ pop_char_t Char;
* ними: для стража Opp = Kid, для Кида Opp = Guard. */ * ними: для стража Opp = Kid, для Кида Opp = Guard. */
pop_char_t Opp; pop_char_t Opp;
/* Снимок последовательности на момент входа в окно Char: по нему void pop_loadkid(void) { Char = Kid; }
* 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_savekid(void) { Kid = Char; } void pop_savekid(void) { Kid = Char; }
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально /* Выгрузка после control() — теперь ПОЛНАЯ, как savekid оригинала.
* меняет у активного персонажа последовательность и состояние меча.
* *
* Почему не `Kid = Char`, как делает savekid оригинала: у нас часть * Была выборочной (только curr_seq и sword) ровно потому, что pop_map правил
* действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq, * геометрию ПРЯМО в Kid, мимо окна Char: полное копирование затирало его
* safe_step...), и он пишет ПРЯМО в Kid там ещё не портирован переход на * правки Kid терял выравнивание x и ряд, и спуск с уступа через вис не
* Char. Полное копирование затирало эти правки: Kid терял выравнивание x * срабатывал (персонаж просто приседал). Приходилось ещё и сравнивать
* и ряд, и спуск с уступа через вис не срабатывал персонаж просто * последовательность со снимком на входе в окно, чтобы понять, чья правка
* приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */ * свежее. С переводом физики на Char (GUARD-PHYS) диспетчер и pop_map
void pop_savekid_state(void) * пишут в одно и то же место, и разбирать это больше не нужно. */
{ void pop_savekid_state(void) { Kid = Char; }
/* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в 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;
}
void pop_loadshad(void) { Char = Guard; } void pop_loadshad(void) { Char = Guard; }
void pop_saveshad(void) { Guard = Char; } void pop_saveshad(void) { Guard = Char; }
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; } void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
@@ -323,9 +318,20 @@ static void load_frame(void)
{ {
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у /* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
* стража frame_tbl_guard с индексом frame + add_frame 149, где * стража 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; 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; int16_t idx = (int16_t)Char.frame;
if (idx >= 102 && idx < 107) idx += 70; if (idx >= 102 && idx < 107) idx += 70;
idx -= 149; idx -= 149;
@@ -473,21 +479,18 @@ void play_seq(void)
* check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и * check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и
* зацеп». Результат менялся от кадра к кадру вместе с анимацией стража. * зацеп». Результат менялся от кадра к кадру вместе с анимацией стража.
* *
* determine_col у нас существует только для Кида: pop_map работает с Kid * determine_col теперь общий: pop_map работает с Char, поэтому колонка
* напрямую, а не с абстрактным Char. Поэтому вторую половину связки делаем * пересчитывается ЛЮБОМУ активному персонажу ровно как load_fram_det_col
* только на его ветке стражу пока хватает перезагрузки самого кадра (его * в оригинале. Раньше вторая половина связки работала только на ветке Кида
* колонку ведёт pop_guard_enter/leave). Char.curr_col синхронизируем * (стражу колонку вёл pop_guard_enter/leave) прямое следствие того, что
* руками: окно Char копия Kid. */ * физика знала один Kid. */
void pop_load_fram_det_col(void) void pop_load_fram_det_col(void)
{ {
if (!kdat_ok) return; if (!kdat_ok) return;
gfx_w0_map(kdat_page); gfx_w0_map(kdat_page);
load_frame(); load_frame();
gfx_w0_unmap(); gfx_w0_unmap();
if (Char.charid == CHARID_0_KID) {
pop_determine_col(); pop_determine_col();
Char.curr_col = Kid.curr_col;
}
} }
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки /* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
@@ -728,7 +731,14 @@ void kid_draw(void)
uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */ uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */
/* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше /* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше
* линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает * линии полосы своего ряда НЕ рисуется. Иначе 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); ct = pop_clip_char_top(kid_fp_x, obj_y, w, h);
if (ct > top) { if (ct > top) {
skip = ct - top; skip = ct - top;
File diff suppressed because it is too large Load Diff
+33 -7
View File
@@ -19,12 +19,21 @@
* что и в pop_room_draw, чтобы отрисовка видела изменения. */ * что и в pop_room_draw, чтобы отрисовка видела изменения. */
void pop_map_set(uint8_t *fg) __banked; void pop_map_set(uint8_t *fg) __banked;
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии: /* Связи комнаты (0=нет соседа; иначе номер). Заодно кэширует fg соседей
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка 2) * слева и справа ЦЕЛИКОМ (читает уровень сам). Оригинал добирается до любой
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая * колонки через find_room_of_tile, у нас уровень в EMM-странице и маппить её
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */ * на каждый get_tile (горячий путь физики) дорого поэтому копия соседей.
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, * Колонка персонажа считается из x и на диапазоне x 0..255 лежит в 5..13,
const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked; * то есть заведомо выходит за комнату; кромочных колонок не хватало, и
* 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). /* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
* Звать в enter_room. */ * Звать в enter_room. */
@@ -125,9 +134,26 @@ extern uint8_t pop_fell_out;
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed + /* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
* determine_col + check_action (триггер падения / do_fall / приземление). * determine_col + check_action (триггер падения / do_fall / приземление).
* Порядок как в оригинале (seg000 play_kid_frame). */ * Порядок как в оригинале (seg000 play_kid_frame).
*
* Физика внутри работает с АКТИВНЫМ персонажем (Char), как в оригинале, и
* сама открывает окно loadkid/savekid звать вне окон Char. */
void pop_phys_tick(void) __banked; 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), верхняя граница клипа спрайта персонажа: если тайл /* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
* над головой стена/пол, всё выше линии своей полосы не рисуется (Kid, * над головой стена/пол, всё выше линии своей полосы не рисуется (Kid,
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели). * ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
+6
View File
@@ -14,6 +14,12 @@
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */ #include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
uint8_t pop_loose_landed; 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) ------------------------------ * /* ---- Шина управления (seg006 globals) ------------------------------ *
* Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка * Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка
+11
View File
@@ -29,6 +29,17 @@ extern uint8_t pop_immortal;
extern int pop_leveldoor_right; extern int pop_leveldoor_right;
extern int pop_leveldoor_ybottom; 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. /* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c.
* Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на * Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на
* _pop_dbg_trap (резидент W1, адрес однозначен). */ * _pop_dbg_trap (резидент W1, адрес однозначен). */
+2 -1
View File
@@ -4,6 +4,7 @@
* См. pop_trob.h. * См. pop_trob.h.
*/ */
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_state.h" /* pop_leveldoor_open */
#include "pop_level.h" #include "pop_level.h"
#include "pop_geom.h" #include "pop_geom.h"
#include "pop_redraw.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 *m = (uint8_t)(*m - sp);
} else { } else {
if (*m < 43) (*m)++; if (*m < 43) (*m)++;
if (*m >= 43) { *m = 43; *type = -1; } if (*m >= 43) { *m = 43; *type = -1; pop_leveldoor_open = 1; }
} }
} }
+1 -1
View File
@@ -14,7 +14,7 @@
* прятаться числами по трём модулям. * прятаться числами по трём модулям.
* 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа. * 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа.
* *
* Пока это только `#define` ни INI, ни меню нет (см. TASKS.md, запись * Пока это только `#define` ни INI, ни меню нет (см. TASKS_OPEN.md, запись
* TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг * TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг
* заводится ЗДЕСЬ, а не константой по месту. Имена как у SDLPoP * заводится ЗДЕСЬ, а не константой по месту. Имена как у SDLPoP
* (custom->имя), чтобы сверка с оригиналом оставалась механической. * (custom->имя), чтобы сверка с оригиналом оставалась механической.
+69 -7
View File
@@ -74,7 +74,7 @@ uint8_t pop_cheats;
* теряется примерно в 44 % случаев, поэтому байты копятся и один из * теряется примерно в 44 % случаев, поэтому байты копятся и один из
* полусотни пропадает ДО чтения порта: нажатие «не срабатывает». * полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
* *
* Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько * Что НЕ помогает (замерено, TASKS_CLOSED.md/KBD-1): редкий опрос (несколько
* вызовов за кадр они попадают туда, где прерывания и так разрешены, и * вызовов за кадр они попадают туда, где прерывания и так разрешены, и
* лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще * лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
* без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя машина * без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя машина
@@ -98,7 +98,9 @@ static void kbd_idle(void)
#define ROOM_1 1 #define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ #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_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
@@ -259,16 +261,25 @@ static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
* активной страницу 1 вызывающий восстанавливает нужную draw-страницу. */ * активной страницу 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; 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; uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
pop_map_set(room_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_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
pop_room_link(room, 2), pop_room_link(room, 3), pop_room_link(room, 2), pop_room_link(room, 3));
lcol_fg, rcol_fg);
pop_room_set_below(below_fg); pop_room_set_below(below_fg);
/* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика /* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика
* уровня, mod живой room_modif (ленивая инициализация). */ * уровня, mod живой room_modif (ленивая инициализация). */
@@ -296,6 +307,7 @@ static void enter_room(uint8_t room)
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
if (!follow)
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */ pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
@@ -325,6 +337,10 @@ static void enter_room(uint8_t room)
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */ 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 /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
* (seg003:028A) поставить Кида в стартовую позицию УРОВНЯ. Один вход и * (seg003:028A) поставить Кида в стартовую позицию УРОВНЯ. Один вход и
* для запуска, и для рестарта после смерти / выпадения из уровня: в * для запуска, и для рестарта после смерти / выпадения из уровня: в
@@ -378,6 +394,7 @@ static uint8_t pop_checkpoint;
static void pop_start_level(void) 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 entry_pose = pop_level_entry_pose(pop_current_level);
uint8_t start_room = pop_level_start_room(); uint8_t start_room = pop_level_start_room();
uint8_t pos = pop_level_start_pos(); uint8_t pos = pop_level_start_pos();
@@ -444,6 +461,7 @@ int main(void)
* по умолчанию (без мерцания); SPACE переключает в однобуфер для * по умолчанию (без мерцания); SPACE переключает в однобуфер для
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint8_t autofrz = 0; /* ВРЕМЕННО: автостоп на падении уже сработал */
uint16_t dead_frames = 0; uint16_t dead_frames = 0;
#ifdef ROOMNAV #ifdef ROOMNAV
uint8_t nav_prev = 0; uint8_t nav_prev = 0;
@@ -525,6 +543,22 @@ int main(void)
* стоит, можно спокойно разбирать позу Kid и окклюзию. */ * стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0; 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)). Все — по { /* Читы (seg000: switch под if (cheats_enabled)). Все — по
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр. * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K убить стража; * K убить стража;
@@ -592,6 +626,14 @@ int main(void)
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1)); ((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
if (nav && !nav_prev) { if (nav && !nav_prev) {
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF; 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) r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
: (uint8_t)(r == 1 ? 24 : r - 1); : (uint8_t)(r == 1 ? 24 : r - 1);
enter_room(r); enter_room(r);
@@ -616,6 +658,7 @@ int main(void)
back = dbuf ? (uint8_t)(front ^ 1) : 0; back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back); gfx_set_draw_page(back);
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
@@ -625,11 +668,13 @@ int main(void)
kid_tick(); /* play_seq: следующий кадр */ kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */ pop_phys_tick(); /* K3: падение/приземление/стена */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */ pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить /* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести HP. */ * попадания с обеих сторон, потом применить, потом свести HP. */
pop_check_sword_hurting(); pop_check_sword_hurting();
pop_check_sword_hurt(); pop_check_sword_hurt();
pop_do_delta_hp(); pop_do_delta_hp();
pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0; pop_fell_out = 0;
@@ -643,6 +688,7 @@ int main(void)
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */ } else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1)); 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 left = (uint8_t)(pop_leave_dir == 1);
uint8_t pop_leave_dir_prev = pop_leave_dir;
pop_leave_dir = 0; pop_leave_dir = 0;
/* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал /* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал
* зовёт его из leave_room ДО goto_other_room, то есть Char.room * зовёт его из leave_room ДО goto_other_room, то есть Char.room
@@ -662,7 +708,9 @@ int main(void)
hitp_beg_lev = hitp_max; hitp_beg_lev = hitp_max;
} }
if (nb) { 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); gfx_set_draw_page(back);
} }
} }
@@ -705,6 +753,13 @@ int main(void)
if (pop_level_load_num(pop_next_level) != 0) { if (pop_level_load_num(pop_next_level) != 0) {
pop_next_level = pop_current_level; /* файла нет — остаёмся */ pop_next_level = pop_current_level; /* файла нет — остаёмся */
} else { } else {
/* Набор спрайтов соперника у каждого уровня СВОЙ
* (load_lev_spr, seg000:1092): уровень 3 скелет, у него и
* кадров меньше, чем у стража. Без перезагрузки соперник
* рисуется чужим атласом: часть индексов уходит за его
* пределы, и персонаж просто не виден. */
pop_guard_free();
pop_guard_load();
/* Всё состояние прошлого уровня сносит сам pop_start_level: /* Всё состояние прошлого уровня сносит сам pop_start_level:
* pop_trob_reset обнуляет room_seen (модификаторы комнат * pop_trob_reset обнуляет room_seen (модификаторы комнат
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
@@ -783,6 +838,13 @@ int main(void)
PROF(6); /* kid_draw (тот же циан) */ PROF(6); /* kid_draw (тот же циан) */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
if (g_after) pop_guard_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-порядок) */ pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
+12 -1
View File
@@ -8,6 +8,11 @@ ENGINE_DIR := $(abspath $(CURDIR)/..)
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код # мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88). # набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88).
CODE_LOC := 0x1000 CODE_LOC := 0x1000
# Данные — выше 0x8000: набор t_phys линкует уже полдвижка (геометрия, Kid,
# карта/физика, управление, страж) и его код перевалил за 0x8000. Раньше
# это проявлялось как «тест зациклился»: хвост кода ложился поверх _DATA.
# 0x9000 оставляет ~24 КБ под код и ~28 КБ до стека (0xFEF0).
DATA_LOC := 0xA000
# Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/ # Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/
# sprinter: под тестом эти функции заглушены (stubs.c), но объявления # 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 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 \ 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_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/stubs.rel build/scene.rel build/kid_blob.rel \
build/bank_stub.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) OBJS_grab := $(OBJS_phys)
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же. # t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
OBJS_wall := $(OBJS_phys) OBJS_wall := $(OBJS_phys)
# t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень).
OBJS_char := $(OBJS_phys)
include $(TESTKIT)/host-tests.mk include $(TESTKIT)/host-tests.mk
@@ -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_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_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 оригинала Главный тест в `t_geom``geom_lcg_matches_reference`. LCG оригинала
(`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме (`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 # переснять все трассы
python3 record_golden.py 7 9 # только сценарии 7 и 9 python3 record_golden.py 7 9 # только сценарии 7 и 9
+48 -4
View File
@@ -19,8 +19,8 @@ sc_rec sc_trace[SC_TRACE_MAX];
uint8_t sc_len; uint8_t sc_len;
static uint8_t sc_fg[30]; static uint8_t sc_fg[30];
/* Кромки шва: соседей у сцены нет, поэтому по краям стена (тайл 20). */ /* Соседей у сцены нет (links = 0) — по краям комнаты стена: карта сама
static const uint8_t sc_edge[6] = { 20, 20, 20, 20, 20, 20 }; * решает так, когда соседняя комната не задана. */
void sc_init(void) void sc_init(void)
{ {
@@ -38,7 +38,7 @@ void sc_room(const uint8_t *fg, uint8_t room)
/* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон /* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон
* и сверху/снизу тогда решения физики зависят только от sc_fg, и * и сверху/снизу тогда решения физики зависят только от 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_start_room = room;
tk_log_clear(); 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_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */ 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(); 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; } void sc_trace_clear(void) { sc_len = 0; }
static void sc_record(void) static void sc_record(void)
@@ -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); 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, записывая трассу. /* Прогнать n кадров с ЗАЖАТОЙ маской keys, записывая трассу.
* Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */ * Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */
uint8_t sc_run(uint8_t keys, uint8_t n); uint8_t sc_run(uint8_t keys, uint8_t n);
+73 -10
View File
@@ -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 pop_heal_fast(int x, int y, int w, int h)
{ (void)x; (void)y; (void)w; (void)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; uint8_t pop_current_level; /* pop_level.c под тест не линкуется; sc_init ставит 1 */
kframe pop_gframe;
int8_t can_guard_see_kid; /* Цвет стража — подмена палитры, живёт в pop_gdraw (отрисовка, в набор не
int8_t guard_refrac, kid_sword_strike; * линкуется). Здесь важен только факт вызова из pop_guard_enter. */
int8_t offguard, holding_sword; void pop_guard_set_palette(uint8_t color) __banked { (void)color; }
/* «Кид нашумел» — физика взводит его на ударе о стену, приземлении и
* обрушенной плите (BUG-GUARD-DEAF-1); читает только автопилот стража, /* Связи комнат: нужны check_guard_fallout (возрождение скелета в комнате
* которого в наборе нет. */ * снизу). Тест задаёт их через tk_link_down. */
int8_t is_guard_notice; 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; }
@@ -40,4 +40,18 @@ void tk_log_clear(void);
/* Сколько записей данного вида в журнале (0 = не было). */ /* Сколько записей данного вида в журнале (0 = не было). */
uint8_t tk_log_count(uint8_t kind); 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 #endif
@@ -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 <stdint.h>
#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);
}
+38 -5
View File
@@ -43,7 +43,8 @@ OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg")
# Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты # Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты
# столбов, ворот, дебриса. См. tile_table.fore_id + анализ pop_background_strategy. # столбов, ворот, дебриса. См. 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): рисуется # Пол-оверлей подъёма (floor_left_overlay, draw_floor_overlay seg008): рисуется
# рантаймом поверх нижней части Kid при подтягивании на пол у кромки — НЕ # рантаймом поверх нижней части Kid при подтягивании на пол у кромки — НЕ
@@ -65,6 +66,23 @@ SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left
134, 135, 136, 137, 138, # right 134, 135, 136, 137, 138, # right
139, 140, 141, 142, 143} # fore 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, # Кадры ОТКРЫТИЯ ворот (portcullis, DOOR_FRAM_SLICE + gate pieces 50/51,
# seg008 draw_gate): 50-59 + 67. В уровнях ворота статически ЗАКРЫТЫ # seg008 draw_gate): 50-59 + 67. В уровнях ворота статически ЗАКРЫТЫ
# (низкий modif → используются только нижние срезы 50-53); срезы более # (низкий 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 (63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal
POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета) POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета)
POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение») 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: при левом соседе-факеле # Основание факела (seg008:489 draw_tile_right: при левом соседе-факеле
# рисуется chtab_6 id 146). В статический render_room не попадает. # рисуется 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 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. """PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60). Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).
@@ -195,7 +214,11 @@ def load_indexed(chtab, id, opaque=False):
if im.mode != "P": if im.mode != "P":
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)") die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
src = im.tobytes() 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) out = bytes((pal_base + v) for v in src)
else: else:
out = bytes((0xFF if v == 0 else pal_base + v) for v in src) out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
@@ -363,6 +386,11 @@ def main():
| GATE_ANIM_ENV_IDS # + кадры открытия ворот | GATE_ANIM_ENV_IDS # + кадры открытия ворот
| TORCH_ENV_IDS # + основание факела | TORCH_ENV_IDS # + основание факела
| STUCK_ENV_IDS # + нажатая кнопка-closer | 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 без пола слева | OPENER_NOFLOOR_ENV_IDS # + opener без пола слева
| LEVELDOOR_ENV_IDS) # + дверь уровня (створка/лестница) | LEVELDOOR_ENV_IDS) # + дверь уровня (створка/лестница)
wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL) wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL)
@@ -373,7 +401,9 @@ def main():
# ENV фон -> страницы по id>>SHIFT # ENV фон -> страницы по id>>SHIFT
pages = {} pages = {}
for id in env_bg: 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) pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
max_pg = max(pages) if pages else -1 max_pg = max(pages) if pages else -1
env_page_names = [] env_page_names = []
@@ -396,7 +426,10 @@ def main():
print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б") print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б")
# FORE -> idx=id # 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) cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_fore.atl"), fore_entries)
total_bytes += sz total_bytes += sz
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б") print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
+115 -3
View File
@@ -21,6 +21,16 @@ sys.path.insert(0, HERE)
import pop_pack_bg as B import pop_pack_bg as B
from pop_pack_kid import transpose_cols 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") GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD")
PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin") PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin")
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard") OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard")
@@ -31,6 +41,32 @@ MASK = (1 << SHIFT) - 1
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784 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): def load_guard(id):
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp """Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
COLUMN-MAJOR); 0 -> 0xFF (прозрачность). COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
@@ -52,13 +88,86 @@ def load_guard(id):
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor) return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
def guard_palette(): NPAL = 7 # guard_palettes: 7 палитр по 16 цветов (res10.bin)
"""16 цветов (r,g,b 0..63) из res10.bin для COLOR."""
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() 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)] 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 <stdint.h>",
"",
"#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(): def main():
os.makedirs(OUT_DIR, exist_ok=True) os.makedirs(OUT_DIR, exist_ok=True)
pages = {} pages = {}
@@ -77,8 +186,11 @@ def main():
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {})) cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
total += sz total += sz
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {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}") print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) > 1:
select_set(sys.argv[1].upper())
main() main()
+1 -1
View File
@@ -346,7 +346,7 @@ Quick wins:
дошедших make против 9 из 10 без хука. Пользователь на ручной дошедших make против 9 из 10 без хука. Пользователь на ручной
проверке отмечает, что редкие пропуски всё же ощущаются — остаток проверке отмечает, что редкие пропуски всё же ощущаются — остаток
отложен до финальной полировки, следующий шаг описан там же. отложен до финальной полировки, следующий шаг описан там же.
Полный протокол — applications/PoP/roomtest/TASKS.md, KBD-1. Полный протокол — applications/PoP/roomtest/TASKS_CLOSED.md, KBD-1.
Recovery-политика уже терпима к частым overrun'ам (селективный Recovery-политика уже терпима к частым overrun'ам (селективный
wipe, docs/kbd-games.md), но аккорды «держу →, тапнул ↑» и wipe, docs/kbd-games.md), но аккорды «держу →, тапнул ↑» и
«держу Shift, тапаю ←» остаются уязвимы. «держу Shift, тапаю ←» остаются уязвимы.
+1 -1
View File
@@ -366,7 +366,7 @@ FIFO SIO — 3 байта, и рассчитывать на «каждый ба
опрос луча, а это ~2/3 периода кадра. Проверено: 35 нажатий стрелки с опрос луча, а это ~2/3 периода кадра. Проверено: 35 нажатий стрелки с
зажатым Shift → 35 дошедших make против 9 из 10 без хука. Остаточные зажатым Shift → 35 дошедших make против 9 из 10 без хука. Остаточные
редкие потери возможны (пачка целиком внутри DI-окна одного accel-прохода); редкие потери возможны (пачка целиком внутри DI-окна одного accel-прохода);
полный протокол и что делать дальше — `applications/PoP/roomtest/TASKS.md`, полный протокол и что делать дальше — `applications/PoP/roomtest/TASKS_CLOSED.md`,
задача KBD-1. задача KBD-1.
**ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/ **ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/
+1 -1
View File
@@ -13,7 +13,7 @@
* потерян, байты копятся и один теряется. Лечится вычерпыванием по * потерян, байты копятся и один теряется. Лечится вычерпыванием по
* опросу (`kbd_raw_poll` из <kbd_raw.h>) но только ПЛОТНЫМ, раз в * опросу (`kbd_raw_poll` из <kbd_raw.h>) но только ПЛОТНЫМ, раз в
* ~0.5 мс, а не пару раз за кадр. Замер: applications/PoP/roomtest/ * ~0.5 мс, а не пару раз за кадр. Замер: applications/PoP/roomtest/
* TASKS.md, задача KBD-1. * TASKS_CLOSED.md, задача KBD-1.
* *
* ТРЕБОВАНИЯ К ХУКУ: он зовётся в тесном цикле, поэтому обязан быть * ТРЕБОВАНИЯ К ХУКУ: он зовётся в тесном цикле, поэтому обязан быть
* дешёвым и НЕ рисовать (окно W3 в этот момент принадлежит вызывающему, * дешёвым и НЕ рисовать (окно W3 в этот момент принадлежит вызывающему,
+1 -1
View File
@@ -69,7 +69,7 @@ void kbd_raw_sync(void);
* (залипание). * (залипание).
* *
* КАК ЕЁ ЗВАТЬ (замеры 2026-08-01, PoP roomtest см. * КАК ЕЁ ЗВАТЬ (замеры 2026-08-01, PoP roomtest см.
* applications/PoP/roomtest/TASKS.md, KBD-1): * applications/PoP/roomtest/TASKS_CLOSED.md, KBD-1):
* - «несколько вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО * - «несколько вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО
* потери те же, что без них: такие вызовы попадают в участки, где * потери те же, что без них: такие вызовы попадают в участки, где
* прерывания и так разрешены, и лишь дублируют трамплин; * прерывания и так разрешены, и лишь дублируют трамплин;
+1 -1
View File
@@ -4,7 +4,7 @@
* PS/2 Set 2, та же битовая карта _kbdraw_down[], тот же FSM префиксов * PS/2 Set 2, та же битовая карта _kbdraw_down[], тот же FSM префиксов
* _kbdraw_pending просто вызванный из главного цикла приложения. * _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 байта, и рассчитывать на то, что каждый принятый * Приёмный FIFO SIO 3 байта, и рассчитывать на то, что каждый принятый
* байт разбудит нас прерыванием, НЕЛЬЗЯ: * байт разбудит нас прерыванием, НЕЛЬЗЯ:
* - импульс запроса прерывания клавиатуры живёт лишь 32 такта CPU * - импульс запроса прерывания клавиатуры живёт лишь 32 такта CPU
+17 -1
View File
@@ -35,11 +35,16 @@ BUILD := build
# таблиц анимации, которую на устройстве туда приводит маппинг W0. # таблиц анимации, которую на устройстве туда приводит маппинг W0.
# EXTRA_INC — заголовки, которых нет ни в testkit, ни в ENGINE_DIR # EXTRA_INC — заголовки, которых нет ни в testkit, ни в ENGINE_DIR
# (например libc/libbgi, когда модуль под тестом их включает). # (например libc/libbgi, когда модуль под тестом их включает).
# DATA_LOC — та же история сверху: набор, чей код перерос 0x8000, обязан
# отодвинуть данные, иначе линкер молча кладёт хвост кода (_HOME, деления
# из libsdcc) ПОВЕРХ _DATA — тест ведёт себя как «зациклился», хотя логика
# ни при чём. Проверку наложения делает check-overlap ниже.
CODE_LOC ?= 0x0200 CODE_LOC ?= 0x0200
DATA_LOC ?= 0x8000
EXTRA_INC ?= EXTRA_INC ?=
CFLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size -I$(TESTKIT) \ CFLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size -I$(TESTKIT) \
$(if $(ENGINE_DIR),-I$(ENGINE_DIR)) $(EXTRA_INC) $(EXTRA_CFLAGS) $(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))) SUITES := $(patsubst t_%.c,%,$(notdir $(wildcard t_*.c)))
COMMON := $(BUILD)/crt0_ucsim.rel $(BUILD)/tcheck.rel COMMON := $(BUILD)/crt0_ucsim.rel $(BUILD)/tcheck.rel
@@ -60,7 +65,18 @@ all: $(addprefix run-,$(SUITES))
@echo "$(LABEL): все наборы прошли ($(words $(SUITES)))" @echo "$(LABEL): все наборы прошли ($(words $(SUITES)))"
# Прогон одного набора. Раннер сам разбирает tc_result и возвращает код. # Прогон одного набора. Раннер сам разбирает 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 run-%: $(BUILD)/%.ihx
$(call CHECK_OVERLAP,$(BUILD)/$*.noi)
@python3 $(RUNNER) $(UCSIM) $(BUILD)/$*.ihx $(BUILD)/$*.noi @python3 $(RUNNER) $(UCSIM) $(BUILD)/$*.ihx $(BUILD)/$*.noi
# Удобный псевдоним: `make t_geom` вместо `make run-geom`. # Удобный псевдоним: `make t_geom` вместо `make run-geom`.
+1 -1
View File
@@ -1,7 +1,7 @@
/* /*
* kbdpoll измерение ПОТОЛКА «плотного опроса» клавиатуры. * kbdpoll измерение ПОТОЛКА «плотного опроса» клавиатуры.
* *
* Вопрос, на который отвечает тест (applications/PoP/roomtest/TASKS.md, * Вопрос, на который отвечает тест (applications/PoP/roomtest/TASKS_CLOSED.md,
* KBD-1): при зажатом Shift часть нажатий расширенных клавиш теряется * KBD-1): при зажатом Shift часть нажатий расширенных клавиш теряется
* байт не доезжает до порта 0x18, потому что импульс запроса прерывания * байт не доезжает до порта 0x18, потому что импульс запроса прерывания
* пропускается примерно в 44 % случаев и трёхбайтовый FIFO SIO * пропускается примерно в 44 % случаев и трёхбайтовый FIFO SIO