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:
@@ -968,3 +968,205 @@ make-бита для кода `0x6C`. Скратч-байты — хвост `o
|
|||||||
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
|
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
|
||||||
недоступен — вопрос закрыт артефактом, а не рассуждением.
|
недоступен — вопрос закрыт артефактом, а не рассуждением.
|
||||||
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
|
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### <a id="l5-shadow"></a>L5-SHADOW. Тень уровня 5: крадёт зелье — **СДЕЛАНА 2026-08-11**
|
||||||
|
|
||||||
|
Единственная новая механика уровня 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-11**
|
||||||
|
|
||||||
|
> **Что уже работает** (решение пользователя: переносим физику на `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_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="l6-shadow"></a>L6-SHADOW. Тень уровня 6 роняет решётку — СДЕЛАНА 2026-08-11
|
||||||
|
|
||||||
|
**Сцена (уровень 6, комната 1).** Кид жмёт `opener` (1,7) — решётка (1,2)
|
||||||
|
поднимается; он разбегается, прыгает через четырёхтайловую пропасть и
|
||||||
|
цепляется за порожек решётки, чтобы подтянуться. В этот момент тень делает
|
||||||
|
ОДИН осторожный шаг с границы (1,0)-(1,1) на `closer` (1,1), и решётка
|
||||||
|
падает. Обе плиты комнаты ведут на один тайл 12 (проверено по LINKLOC).
|
||||||
|
|
||||||
|
**Порт.** Две точки, обе в `seg002.c`:
|
||||||
|
|
||||||
|
- `check_shadow` (seg002:0090): условий на содержимое комнаты нет — тень
|
||||||
|
встаёт при КАЖДОМ входе в комнату 1. `do_init_shad` переписан под
|
||||||
|
таблицы (`init_shad_5`/`init_shad_6` — первые 7 полей `Char`, как
|
||||||
|
`memcpy(&Char, source, 7)` у оригинала);
|
||||||
|
- `autocontrol_shadow_level6` (seg002:1064): `Kid.frame == 43` (кадр
|
||||||
|
бег-прыжка) и `Kid.x < 128` -> `move_6_shift()` + `move_1_forward()`.
|
||||||
|
Это Shift+вперёд, то есть ОСТОРОЖНЫЙ ШАГ, а не прыжок; кадр 43 —
|
||||||
|
условие на КИДА, тень им только триггерится.
|
||||||
|
|
||||||
|
**Звук** `sound_25_presentation` не портирован (звука нет вовсе); флаг
|
||||||
|
оригинала `leveldoor_open = 0x4D` не воспроизводим — чужую переменную
|
||||||
|
занимать незачем.
|
||||||
|
|
||||||
|
**Проверено в MAME (пользователь, 2026-08-11):** «Кид прыгнул, зацепился,
|
||||||
|
Тень сделал шаг, решётка упала». Тем самым закрыт и остаток
|
||||||
|
[GUARD-PHYS](TASKS_CLOSED.md#guard-phys): нажатие плиты НЕ-Кидом работает
|
||||||
|
(`check_press` в `guard_phys`, без гейта по `charid`).
|
||||||
|
|
||||||
|
**Фон комнаты сверен с оригиналом** методом из
|
||||||
|
[`BUGS_CLOSED.md`](BUGS_CLOSED.md#bug-lattice-doortop): 539 пикселей
|
||||||
|
расхождения, и все — силуэты персонажей (тень пока рисуется палитрой Кида,
|
||||||
|
см. [`../docs/shadow_render.md`](../docs/shadow_render.md)).
|
||||||
|
|
||||||
|
**Цена:** банк 1 2755 -> 2877 Б, резидент без изменений.
|
||||||
|
|||||||
@@ -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**. Дальнейшее — запас, не срочность | плавность на ВСЕХ уровнях |
|
| — | [DRAW-COST](#draw-cost) | **кадр уложился в бюджет 2026-08-09**: 470 964 -> 425 600 тактов, период цикла 4 растровых кадра -> **3**. Дальнейшее — запас, не срочность | плавность на ВСЕХ уровнях |
|
||||||
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
|
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
|
||||||
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
|
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
|
||||||
@@ -89,167 +87,6 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
|
|||||||
|
|
||||||
## P0 — делаем сейчас
|
## 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_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-color"></a>L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный)
|
### <a id="l3-color"></a>L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный)
|
||||||
|
|
||||||
**Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В
|
**Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В
|
||||||
|
|||||||
@@ -224,18 +224,25 @@ static uint8_t demo_index; /* текущая запись
|
|||||||
#define SEQ_2_STAND 2
|
#define SEQ_2_STAND 2
|
||||||
#define TILE_POTION 10
|
#define TILE_POTION 10
|
||||||
|
|
||||||
/* init_shad_5 (data:0EF2): frame 15, x 55, y 55, direction 0 (вправо),
|
/* Таблицы init_shad_N (data:0EEA/0EF2) — ПЕРВЫЕ СЕМЬ полей Char в том же
|
||||||
* curr_col −1 (ещё за левым краем), curr_row 0, action 0 (стоит). */
|
* порядке, в каком их копирует оригинал (`memcpy(&Char, source, 7)`):
|
||||||
static void do_init_shad_5(uint8_t room)
|
* frame, x, y, direction, curr_col, curr_row, action.
|
||||||
|
* 5: x 55, y 55, вправо, колонка −1 (ещё за левым краем экрана), ряд 0;
|
||||||
|
* 6: x 81, y 118, вправо, колонка 0, ряд 1 — тень стоит на границе
|
||||||
|
* (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту. */
|
||||||
|
static const uint8_t init_shad_5[7] = { 0x0F, 0x37, 0x37, 0, 0xFF, 0, 0 };
|
||||||
|
static const uint8_t init_shad_6[7] = { 0x0F, 0x51, 0x76, 0, 0x00, 1, 0 };
|
||||||
|
|
||||||
|
static void do_init_shad(const uint8_t *tbl, uint8_t room)
|
||||||
{
|
{
|
||||||
pop_loadshad();
|
pop_loadshad();
|
||||||
Char.frame = 0x0F;
|
Char.frame = tbl[0];
|
||||||
Char.x = 0x37;
|
Char.x = tbl[1];
|
||||||
Char.y = 0x37;
|
Char.y = tbl[2];
|
||||||
Char.direction = 0;
|
Char.direction = (int8_t)tbl[3];
|
||||||
Char.curr_col = -1;
|
Char.curr_col = (int8_t)tbl[4];
|
||||||
Char.curr_row = 0;
|
Char.curr_row = (int8_t)tbl[5];
|
||||||
Char.action = 0;
|
Char.action = tbl[6];
|
||||||
Char.room = room;
|
Char.room = room;
|
||||||
Char.charid = CHARID_1_SHADOW;
|
Char.charid = CHARID_1_SHADOW;
|
||||||
Char.alive = Kid.alive; /* наследование от Кида, см. шапку */
|
Char.alive = Kid.alive; /* наследование от Кида, см. шапку */
|
||||||
@@ -254,12 +261,22 @@ static void do_init_shad_5(uint8_t room)
|
|||||||
|
|
||||||
uint8_t pop_check_shadow(uint8_t room) __banked
|
uint8_t pop_check_shadow(uint8_t room) __banked
|
||||||
{
|
{
|
||||||
|
/* Уровень 6 (seg002:0090): условий на содержимое комнаты нет вовсе —
|
||||||
|
* тень встаёт при КАЖДОМ входе в комнату 1. Оригинал здесь ещё играет
|
||||||
|
* sound_25_presentation один раз, отмечая это `leveldoor_open = 0x4D`
|
||||||
|
* (переменная двери используется как флаг); звука у нас нет, а чужую
|
||||||
|
* переменную портить незачем — TODO вместе со звуком. */
|
||||||
|
if (pop_current_level == SHADOW_STEP_LEVEL) {
|
||||||
|
if (room != SHADOW_STEP_ROOM) return 0;
|
||||||
|
do_init_shad(init_shad_6, room);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if (pop_current_level != SHADOW_STEAL_LEVEL) return 0;
|
if (pop_current_level != SHADOW_STEAL_LEVEL) return 0;
|
||||||
if (room != SHADOW_STEAL_ROOM) return 0;
|
if (room != SHADOW_STEAL_ROOM) return 0;
|
||||||
/* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража
|
/* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража
|
||||||
* комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */
|
* комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */
|
||||||
if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1;
|
if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1;
|
||||||
do_init_shad_5(room);
|
do_init_shad(init_shad_5, room);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,12 +621,30 @@ static void autocontrol_shadow_level5(void)
|
|||||||
if (Char.x < 15) clear_char();
|
if (Char.x < 15) clear_char();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* autocontrol_shadow (seg002:081D) — по уровням. Ветки 6 (шагает по плите)
|
/* autocontrol_shadow_level6 (seg002:1064). Вся логика — один осторожный шаг
|
||||||
* и 12 появятся со своими уровнями. */
|
* (Shift + вперёд) в тот кадр, когда Кид прыгает к решётке: тень встаёт на
|
||||||
|
* closer (1,1) и роняет решётку (1,2), за порожек которой Кид в этот момент
|
||||||
|
* цепляется. Условие смотрит на КИДА: кадр 43 — четвёртый кадр бег-прыжка,
|
||||||
|
* x < 128 отсекает прыжки в правой половине комнаты (там Кид ещё разбегается
|
||||||
|
* и до решётки ему далеко). */
|
||||||
|
#define FRAME_43_RUNJUMP 43
|
||||||
|
|
||||||
|
static void autocontrol_shadow_level6(void)
|
||||||
|
{
|
||||||
|
if (Char.room != SHADOW_STEP_ROOM) return;
|
||||||
|
if (Kid.frame != FRAME_43_RUNJUMP) return;
|
||||||
|
if (Kid.x >= 128) return;
|
||||||
|
move_6_shift();
|
||||||
|
move_1_forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* autocontrol_shadow (seg002:081D) — по уровням. Ветка 12 появится со своим
|
||||||
|
* уровнем. */
|
||||||
static void autocontrol_shadow(void)
|
static void autocontrol_shadow(void)
|
||||||
{
|
{
|
||||||
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
|
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
|
||||||
if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5();
|
if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5();
|
||||||
|
if (pop_current_level == SHADOW_STEP_LEVEL) autocontrol_shadow_level6();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
|
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
|
||||||
|
|||||||
@@ -94,6 +94,15 @@
|
|||||||
#define SHADOW_DOOR_TILEPOS 1 /* (кол 1, ряд 0) — ворота */
|
#define SHADOW_DOOR_TILEPOS 1 /* (кол 1, ряд 0) — ворота */
|
||||||
#define SHADOW_DOOR_OPEN_MIN 80 /* openness, при которой тень идёт */
|
#define SHADOW_DOOR_OPEN_MIN 80 /* openness, при которой тень идёт */
|
||||||
|
|
||||||
|
/* Спецсобытие «тень захлопывает решётку» (custom->shadow_step_*, дефолты
|
||||||
|
* SDLPoP): уровень 6, комната 1. Кид жмёт opener (1,7), решётка (1,2)
|
||||||
|
* поднимается, он прыгает через пропасть и цепляется за её порожек — и
|
||||||
|
* ровно в этот момент тень делает ОДИН осторожный шаг с границы (1,0)-(1,1)
|
||||||
|
* на closer (1,1), роняя решётку. Обе плиты комнаты ведут на тот же тайл
|
||||||
|
* 12 (проверено по LINKLOC/LINKMAP уровня). */
|
||||||
|
#define SHADOW_STEP_LEVEL 6
|
||||||
|
#define SHADOW_STEP_ROOM 1
|
||||||
|
|
||||||
/* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех
|
/* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех
|
||||||
* остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см.
|
* остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см.
|
||||||
* pop_check_can_guard_see_kid. */
|
* pop_check_can_guard_see_kid. */
|
||||||
|
|||||||
@@ -226,7 +226,14 @@ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
|
|||||||
? (uint8_t)(room_fg_ptr(dl)[9] & 0x1F)
|
? (uint8_t)(room_fg_ptr(dl)[9] & 0x1F)
|
||||||
: 20; /* кромка уровня */
|
: 20; /* кромка уровня */
|
||||||
} else {
|
} else {
|
||||||
for (i = 0; i < 11; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */
|
/* Комнаты снизу НЕТ. Оригинал (load_rowbelow, seg008:368) в этом
|
||||||
|
* случае подставляет РАЗНЫЕ кромки: колонкам 1..9 — `tiles_0_empty`,
|
||||||
|
* и только левому краю (колонка 0, тайл из room_BL) — `tiles_20_wall`.
|
||||||
|
* У нас тут стояла стена на всё, и её грань (topright стены) лезла
|
||||||
|
* жёлтыми треугольниками под пустые тайлы нижнего ряда — видно в
|
||||||
|
* шахте падения, уровень 6 комната 3 (BUG-BELOWROW-WALL). */
|
||||||
|
for (i = 0; i < 10; i++) below_fg[i] = 0; /* tiles_0_empty */
|
||||||
|
below_fg[10] = 20; /* левая кромка уровня */
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ static void draw_tile(int row, int col)
|
|||||||
* «стена» там стоит, только если такой комнаты нет — как в
|
* «стена» там стоит, только если такой комнаты нет — как в
|
||||||
* load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */
|
* load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */
|
||||||
if (lc < 0) rbl_code = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20;
|
if (lc < 0) rbl_code = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20;
|
||||||
else rbl_code = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 1;
|
else rbl_code = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 0;
|
||||||
/* Модификатора ряда снизу у нас нет (pop_t_below — только fg), а он
|
/* Модификатора ряда снизу у нас нет (pop_t_below — только fg), а он
|
||||||
* нужен паласной панели над воротами. Ставим 0 = «панели нет»:
|
* нужен паласной панели над воротами. Ставим 0 = «панели нет»:
|
||||||
* случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого
|
* случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого
|
||||||
|
|||||||
Reference in New Issue
Block a user