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:
@@ -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`).
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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` (таблицы последовательностей),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
|
||||||
|
|||||||
@@ -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. Документ самодостаточный: рассчитан на старт
|
||||||
«с чистого листа» (пустой контекст). Всё сверено с
|
«с чистого листа» (пустой контекст). Всё сверено с
|
||||||
|
|||||||
@@ -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 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
|
||||||
|
|||||||
@@ -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 — это правая половина
|
||||||
|
|||||||
@@ -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.
@@ -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`, а в открытом файле остаётся ссылка.
|
||||||
|
Открытые файлы обязаны читаться целиком за раз.
|
||||||
|
|
||||||
## Сборка и запуск
|
## Сборка и запуск
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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_room−1] >= 30 || // в новой комнате
|
||||||
|
guards_seq_hi[kid_room−1] != 0) { // своего живого нет
|
||||||
|
if (ушёл ВЛЕВО) { if (Guard.x >= 91) leave = 1; } // страж далеко — остаётся
|
||||||
|
else if (ВПРАВО) { if (Guard.x < 165) leave = 1; }
|
||||||
|
else if (ВВЕРХ) { if (Guard.curr_row >= 0) leave = 1; } // всегда → не идёт
|
||||||
|
else { if (Guard.curr_row < 3) leave = 1; } // вниз → не идёт
|
||||||
|
} else leave = 1;
|
||||||
|
} else leave = 1;
|
||||||
|
leave ? leave_guard() : follow_guard();
|
||||||
|
```
|
||||||
|
|
||||||
|
`follow_guard` (seg002:039E) стирает `guards_tile` в ОБЕИХ комнатах
|
||||||
|
(0xFF — «стража здесь нет», чтобы он не раздвоился) и гонит стража через
|
||||||
|
`goto_other_room` в окне `loadshad`/`saveshad`.
|
||||||
|
|
||||||
|
**Что это значит для нас.** У нас в `enter_room` (`roomtest.c:265`) стоит
|
||||||
|
безусловный `pop_guard_leave()` — то есть всегда ветка `leave_guard`, и
|
||||||
|
страж ВСЕГДА остаётся. Портировать надо сам `exit_room`-выбор: условия
|
||||||
|
«жив + меч вынут + в целевой комнате нет своего стража + он у нужного
|
||||||
|
края» и `follow_guard`. Пороги 91/165 — это «страж у того края, в который
|
||||||
|
ушёл Кид»; вверх и вниз оригинал не пускает никогда.
|
||||||
|
|
||||||
|
**Чем подтверждать:** уровень 1, комната 3 — начать бой, отступить влево в
|
||||||
|
комнату 2: страж обязан прийти следом и продолжить бой (как в SDLPoP на
|
||||||
|
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
|
||||||
|
живой страж — переход не происходит.
|
||||||
|
|
||||||
|
### <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).
|
||||||
@@ -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[room−1] & 0x0F` (enter_guard,
|
||||||
|
seg002:184), а `guard_palettes` — ресурс 10 из `PRINCE.DAT` (7 палитр × 16
|
||||||
|
цветов, 6-битные каналы). У нас `pop_pack_guard.py` брал `COLOR = 2`
|
||||||
|
константой (цвет стражей уровня 1) и запекал одну палитру в слоты
|
||||||
|
`0x90..0x9F`. Полоса HP рисуется тем же атласом — отдельного бага не было.
|
||||||
|
|
||||||
|
**Фикс.**
|
||||||
|
1. `pop_pack_guard.py` выгружает ВСЕ 7 палитр в `pop_guard_pal.h`
|
||||||
|
(7 × 16 × 4 = 448 Б, записи `B,G,R,0` — формат `gfx_pal_load`);
|
||||||
|
каналы масштабируются `scale6to8`, как в `pop_pack_kid` для `kid.pal`.
|
||||||
|
2. `pop_guard_set_palette(color)` (`pop_gdraw.c`) заливает 16 слотов в ОБЕ
|
||||||
|
палитры дабл-буфера; `color == 0` — не трогать (так и оригинал).
|
||||||
|
3. Зовётся из `pop_guard_enter` сразу после чтения данных комнаты, то есть
|
||||||
|
ДО отрисовки — как `redraw_screen`. `pop_level_guard` цвет отдавал уже
|
||||||
|
давно, его просто никто не использовал.
|
||||||
|
|
||||||
|
**Грабли, стоившие итерации (записать на будущее).** Первая версия передавала
|
||||||
|
`gfx_pal_load` указатель прямо на таблицу — а таблица лежит в rodata
|
||||||
|
БАНКОВОГО модуля, то есть по 0xC000+. `gfx_pal_load` отдаёт указатель в BIOS
|
||||||
|
(`$A4` через `rst #0x08`), а BIOS читает только `#4000-#BFFF` (корневой
|
||||||
|
CLAUDE.md). BIOS забирал мусор с чужой страницы, палитра уезжала в тёмное и
|
||||||
|
страж становился НЕВИДИМЫМ. Лечится копией записи в локальный буфер — стек
|
||||||
|
гарантированно в W2.
|
||||||
|
|
||||||
|
**Проверено в MAME (уровень 2, ROOMNAV):**
|
||||||
|
|
||||||
|
| комната | цвет из уровня | что на экране |
|
||||||
|
|---------|----------------|---------------|
|
||||||
|
| 11 | 1 | страж сине-фиолетовый, полоса HP синяя |
|
||||||
|
| 7 | 3 | страж оранжевый, полоса HP оранжевая |
|
||||||
|
| ур. 1 | 2 | охра, как было (регрессии нет) |
|
||||||
|
|
||||||
|
Цвета сходятся с таблицей `pop_guard_pal.h`: цвет 1 = (72,145,255),
|
||||||
|
цвет 3 = (255,80,0), цвет 2 = (170,48,0).
|
||||||
|
|
||||||
|
**Смежное, НЕ входит сюда:** палитра КЛАДКИ уровня 3 (в оригинале зелёная) —
|
||||||
|
другой механизм и другой ресурс, заведена отдельной задачей
|
||||||
|
[L3-COLOR](TASKS_OPEN.md#l3-color).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<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_y−3), подъём по 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) —
|
||||||
|
отдельной работы не требует.
|
||||||
|
|||||||
@@ -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_y−3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
|
|
||||||
| 16 | | |
|
|
||||||
| 17 | | |
|
|
||||||
| 18 | недостижима в игре | |
|
|
||||||
| 19 | | |
|
|
||||||
| 20 | | |
|
|
||||||
| 21 | | |
|
|
||||||
| 22 | | |
|
|
||||||
| 23 | | |
|
|
||||||
| 24 | недостижима в игре | |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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 | закрыт |
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Баги/недоработки на уровне 2
|
||||||
|
|
||||||
|
- комната 4 - бой со стражем - страж другого цвета/другого цвета у него HP (сравнение с SDLPoP)
|
||||||
|
- комната 4 - бой со стражем - при нанесении урона стражу должен рисоваться сплеш (типа звезды) - что бы игрок
|
||||||
|
видел что удар дошел до цели без перевода внимания на HP (
|
||||||
|
|
||||||
|
- баг нашего cheats - если мы нажимаем +/- (переход по комнатам) в момент когда Кид вытащил меч для битвы -
|
||||||
|
Кид теряет управление (он переключается в режим боя но в новой комнате нету стража - в результате нажатия
|
||||||
|
кнопок движения игнорятся)
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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/полоса потолка): он выходит за
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) --- *
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
+433
-266
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||||
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
|
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
|
||||||
|
|||||||
@@ -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, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка —
|
||||||
|
|||||||
@@ -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, адрес однозначен). */
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* прятаться числами по трём модулям.
|
* прятаться числами по трём модулям.
|
||||||
* 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа.
|
* 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа.
|
||||||
*
|
*
|
||||||
* Пока это только `#define` — ни INI, ни меню нет (см. TASKS.md, запись
|
* Пока это только `#define` — ни INI, ни меню нет (см. TASKS_OPEN.md, запись
|
||||||
* TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг
|
* TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг
|
||||||
* заводится ЗДЕСЬ, а не константой по месту. Имена — как у SDLPoP
|
* заводится ЗДЕСЬ, а не константой по месту. Имена — как у SDLPoP
|
||||||
* (custom->имя), чтобы сверка с оригиналом оставалась механической.
|
* (custom->имя), чтобы сверка с оригиналом оставалась механической.
|
||||||
|
|||||||
@@ -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) */
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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} Б")
|
||||||
|
|||||||
@@ -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
@@ -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, тапаю ←» остаются уязвимы.
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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 в этот момент принадлежит вызывающему,
|
||||||
|
|||||||
@@ -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):
|
||||||
* - «несколько вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО —
|
* - «несколько вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО —
|
||||||
* потери те же, что без них: такие вызовы попадают в участки, где
|
* потери те же, что без них: такие вызовы попадают в участки, где
|
||||||
* прерывания и так разрешены, и лишь дублируют трамплин;
|
* прерывания и так разрешены, и лишь дублируют трамплин;
|
||||||
|
|||||||
@@ -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
@@ -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,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
|
||||||
|
|||||||
Reference in New Issue
Block a user