L6-SHADOW: тень роняет решётку + кромка ряда ниже

1. L6-SHADOW (seg002:0090 + seg002:1064).  Тень встаёт при каждом входе в
   комнату 1 уровня 6 и делает ОДИН осторожный шаг (Shift+вперёд) в тот
   кадр, когда Кид прыгает к решётке (Kid.frame == 43, Kid.x < 128): встаёт
   на closer (1,1) и роняет решётку (1,2), за порожек которой Кид цепляется.
   do_init_shad переписан под таблицы init_shad_5/6 — первые 7 полей Char,
   как memcpy(&Char, source, 7) у оригинала.

   Проверено в MAME: «Кид прыгнул, зацепился, Тень сделал шаг, решётка
   упала».  Тем самым закрыт остаток GUARD-PHYS — нажатие плиты НЕ-Кидом
   работает.

2. BUG-BELOWROW-WALL.  При отсутствующей комнате снизу load_rowbelow
   (seg008:368) подставляет tiles_0_empty колонкам 1..9 и tiles_20_wall
   только левому краю; у нас стеной забивались все 11 позиций, и её
   topright лез жёлтыми треугольниками под пустые тайлы нижнего ряда
   (видно в шахте падения, комната 3).

Комнаты 1 и 3 уровня 6 сверены с картой SDLPoP: расхождения только силуэты
персонажей.  tests-host 5/5, size-check чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:20:12 +03:00
parent 740cc6d652
commit e4ef489872
6 changed files with 268 additions and 178 deletions
-163
View File
@@ -55,8 +55,6 @@
| # | Задача | Что | Блокирует |
|---|--------|-----|-----------|
| 1 | [L5-SHADOW](#l5-shadow) | **СЛЕДУЮЩАЯ**: тень уровня 5 (крадёт зелье в комнате 24) + движок автодвижений | прохождение ур. 5 |
| 2 | [GUARD-PHYS](#guard-phys) | физика стража = физика Кида — ядро сделано, остаток: ветка ТЕНИ в `check_guard_fallout` и живая проверка кнопки под стражем | ур. 5+ (тень) |
| — | [DRAW-COST](#draw-cost) | **кадр уложился в бюджет 2026-08-09**: 470 964 -> 425 600 тактов, период цикла 4 растровых кадра -> **3**. Дальнейшее — запас, не срочность | плавность на ВСЕХ уровнях |
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
@@ -89,167 +87,6 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
## P0 — делаем сейчас
### <a id="l5-shadow"></a>L5-SHADOW. Тень уровня 5: крадёт зелье — **СЛЕДУЮЩАЯ**
Единственная новая механика уровня 5 (тайлов новых нет вовсе, см. цель выше).
Тень появляется в комнате 24, дожидается, пока откроется дверь, идёт к зелью,
**выпивает его** и уходит за левый край. Боя нет.
**Как это в оригинале** (всё сверено по коду, `custom->*` — это дефолты 1.0):
| что | где | суть |
|---|---|---|
| появление | `check_shadow`, seg002:0064 | при СМЕНЕ КОМНАТЫ: если `current_level == 5` и `drawn_room == 24`, и тайл (кол 3, ряд 0) всё ещё `tiles_10_potion` — породить тень |
| порождение | `do_init_shad`, seg002:0000 | `memcpy(&Char, init_shad_5, 7)` + `seqtbl_offset_char(2 /*stand*/)`, `charid = charid_1_shadow`, `demo_time = 0`, `guard_skill = 3`, `guardhp_* = 4`, `saveshad()` |
| данные | `init_shad_5` | `{0x0F, 0x37, 0x37, 0, 0xFF, 0, 0}` = frame 15, x 55, y 55, direction 0, curr_col 1, curr_row 0, action 0 |
| поведение | `autocontrol_shadow_level5`, seg002:1157 | в комнате 24: пока `demo_time == 0` — ждать, пока дверь (кол 1, ряд 0) не откроется (`modif >= 80`), затем `demo_index = 0`; дальше каждый кадр `do_auto_moves(shad_drink_move)`; при `Char.x < 15``clear_char()` |
| движения | `do_auto_moves`, seg002:1089 | крошечный интерпретатор: `demo_time++`, по таблице `{time, move}` выбирается запись, `move` = 0 nothing / 1 forward / 2 backward / 3 up / 4 down / 5 up+forward / 6 shift / 7 move_7; 1 = ничего, −2 = конец |
| таблица | `shad_drink_move` (data.h:866) | `{0x00,0} {0x01,1} {0x0E,0} {0x12,6} {0x1D,7} {0x2D,2} {0x31,1} {0xFF,2}` — 8 записей по 2 байта |
**Что из этого у нас уже есть:**
- **механизм «спецсобытие порождает персонажа в слоте соперника»** —
`pop_check_skel` (`guards.c`, зовётся из `roomtest.c` в тике); тень
уровня 5 садится на тот же шов, только условие другое;
- **тень как `charid_1_shadow`** — заведена под уровень 4
([L4-MIRROR](TASKS_CLOSED.md#l4-mirror)): своя ветка ИИ
(`autocontrol_shadow` + `autocontrol_shadow_level4`), выбор таблицы кадров
Кида (`pop_frame_tbl_is_guard`), отрисовка спрайтами Кида;
- **питьё зелья** — `SEQ_78_DRINK` и `get_item` в `pop_ctrl.c` (Кид уже
умеет); тень «нажимает» те же кнопки через автодвижения;
- **зелья как trob** — фаза пузырька, тип в старших битах (`pop_trob.c`).
**Что писать:**
1. `do_auto_moves` + таблица `shad_drink_move` + `demo_time`/`demo_index`
интерпретатор ~30 строк, кладётся рядом с `autocontrol_shadow` в
`guards.c` (банк 1). «Движения» — это те же переменные управления, что
заполняет `read_user_control` (`pop_ctrl.c`), так что `move_*` сводятся к
присваиваниям.
2. `do_init_shad(init_shad_5, seq stand)` — общий порождатель тени; пригодится
и на уровнях 6 и 12 (`init_shad_6`, `init_shad_12` — те же 7 байт).
3. Ветка `check_shadow` для уровня 5 — по образцу `pop_check_skel`, вызов из
того же места тика.
4. `autocontrol_shadow_level5`.
5. **Ветка ТЕНИ в `check_guard_fallout`** (seg002:0241): тень падает, только
если она в свободном полёте (`action == 4`), и тогда
`loadshad(); clear_char(); saveshad()`. Сейчас в `pop_guard_fallout`
(`pop_guard.c`) есть ветки стража и скелета, а тени нет — комментарий там
обещает её «вместе с L3-SKEL», но она относится именно к тени.
**Чем подтверждать:** smoke уровня 5 — дойти до комнаты 24, увидеть, как
тень выходит после открытия двери, выпивает зелье (тайл зелья исчезает) и
уходит влево. Сверять последовательность движений с живым SDLPoP на том же
месте — таблица `shad_drink_move` короткая, расхождение будет видно сразу.
**Оговорка по виду:** тень пока рисуется обычной копией спрайтов Кида, то
есть выглядит вторым Кидом — это [отложенный](../docs/shadow_render.md)
вопрос, к механике уровня 5 отношения не имеет.
### <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](BUGS_CLOSED.md#bug-sword-ghost-1): при переходе в бою Кид
> прячет меч и дальше дерётся пустой рукой.**
>
> **Осталось (потому и запись открыта) — ревизия 2026-08-11 по коду:**
> 1. ~~`check_chomped_guard`~~ — **сделан** вместе с
> [L3-CHOMP](TASKS_CLOSED.md#l3-chomp) (`pop_map.c`);
> 2. ветки `check_guard_fallout`: **скелет сделан** (возрождается в комнате 3,
> `pop_guard_fallout` в `pop_guard.c`), **ветки ТЕНИ нет** — падает только
> в свободном полёте, `loadshad/clear_char/saveshad`; идёт в
> [L5-SHADOW](#l5-shadow) п. 5 (комментарий в коде обещает её «вместе с
> L3-SKEL» — устарел, это про тень);
> 3. страж, нажимающий напольную кнопку, вживую не проверялся (код —
> общий `check_press`).
**Почему это была ОДНА физика, а не «сделаем стражу свою».** В оригинале
слот `Guard` — не «стражи», а все НЕ-Киды: тем же `play_guard_frame` ходят
**страж** (charid 2), **скелет** (4, ур. 3), **тень** (1, ур. 4/5/6/12),
**визирь-Джаффар** (ур. 13), **толстяк** (FAT, ур. 12) и **мышь** (0x18,
ур. 8) — `tbl_guard_type` = {0,0,0,2,0,0,1,0,0,0,0,0,4,3,1,1}, а
`autocontrol_opponent` (seg002:628) разводит их ТОЛЬКО по ИИ. То есть
второй экземпляр логики пришлось бы делать не один раз, а пять.
**Гард по X: страж не уходит САМ — но его МОГУТ ПЕРЕНЕСТИ.** Поправка к
формулировке, которая была здесь раньше («страж не покидает комнату ни в
каком виде») — она неверна, контрпример дал пользователь: в SDLPoP страж из
комнаты 3 оказывается в комнате 2 вслед за отступающим Кидом.
Разделять надо два разных механизма:
- **своим ходом — не может.** Физика персонажа слота Guard обёрнута
`Char.room == drawn_room` и `Char.x >= 44 && Char.x < 211` (seg000:1252),
и никакого `check_leave` в его списке вызовов нет. Провалившегося ниже
комнаты убирает `check_guard_fallout` (seg002:0241) — вниз он не уходит.
- **следом за Кидом — переносит движок.** `exit_room` (seg002:03C7)
вызывается ПОСЛЕ того, как комнату сменил Кид, и решает судьбу стража:
```c
if (Guard.alive < 0 && Guard.sword == sword_2_drawn) { // жив и В БОЮ
if (guards_tile[kid_room1] >= 30 || // в новой комнате
guards_seq_hi[kid_room1] != 0) { // своего живого нет
if (ушёл ВЛЕВО) { if (Guard.x >= 91) leave = 1; } // страж далеко — остаётся
else if (ВПРАВО) { if (Guard.x < 165) leave = 1; }
else if (ВВЕРХ) { if (Guard.curr_row >= 0) leave = 1; } // всегда → не идёт
else { if (Guard.curr_row < 3) leave = 1; } // вниз → не идёт
} else leave = 1;
} else leave = 1;
leave ? leave_guard() : follow_guard();
```
`follow_guard` (seg002:039E) стирает `guards_tile` в ОБЕИХ комнатах
(0xFF — «стража здесь нет», чтобы он не раздвоился) и гонит стража через
`goto_other_room` в окне `loadshad`/`saveshad`.
**Что это значит для нас.** У нас в `enter_room` (`roomtest.c:265`) стоит
безусловный `pop_guard_leave()` — то есть всегда ветка `leave_guard`, и
страж ВСЕГДА остаётся. Портировать надо сам `exit_room`-выбор: условия
«жив + меч вынут + в целевой комнате нет своего стража + он у нужного
края» и `follow_guard`. Пороги 91/165 — это «страж у того края, в который
ушёл Кид»; вверх и вниз оригинал не пускает никогда.
**Чем подтверждать:** уровень 1, комната 3 — начать бой, отступить влево в
комнату 2: страж обязан прийти следом и продолжить бой (как в SDLPoP на
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
живой страж — переход не происходит.
### <a id="l3-color"></a>L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный)
**Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В