From e4ef48987269fa08fbe69b8203254b48b79b82d3 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 11 Aug 2026 22:20:12 +0300 Subject: [PATCH] =?UTF-8?q?L6-SHADOW:=20=D1=82=D0=B5=D0=BD=D1=8C=20=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D1=8F=D0=B5=D1=82=20=D1=80=D0=B5=D1=88=D1=91=D1=82?= =?UTF-8?q?=D0=BA=D1=83=20+=20=D0=BA=D1=80=D0=BE=D0=BC=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=80=D1=8F=D0=B4=D0=B0=20=D0=BD=D0=B8=D0=B6=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- applications/PoP/roomtest/TASKS_CLOSED.md | 202 ++++++++++++++++++++++ applications/PoP/roomtest/TASKS_OPEN.md | 163 ----------------- applications/PoP/roomtest/guards.c | 61 +++++-- applications/PoP/roomtest/pop_guard.h | 9 + applications/PoP/roomtest/pop_level.c | 9 +- applications/PoP/roomtest/pop_room.c | 2 +- 6 files changed, 268 insertions(+), 178 deletions(-) diff --git a/applications/PoP/roomtest/TASKS_CLOSED.md b/applications/PoP/roomtest/TASKS_CLOSED.md index 34d3deb..ebb3610 100644 --- a/applications/PoP/roomtest/TASKS_CLOSED.md +++ b/applications/PoP/roomtest/TASKS_CLOSED.md @@ -968,3 +968,205 @@ make-бита для кода `0x6C`. Скратч-байты — хвост `o работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции недоступен — вопрос закрыт артефактом, а не рассуждением. - Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1. + +--- + +### 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 отношения не имеет. + +--- + +### 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 на +скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ +живой страж — переход не происходит. + +--- + +### 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 Б, резидент без изменений. diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 89b4eb3..c85b6d7 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -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 — делаем сейчас -### 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 отношения не имеет. - -### 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 на -скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ -живой страж — переход не происходит. - ### L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный) **Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index 15f7947..d2dd3e5 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -224,18 +224,25 @@ static uint8_t demo_index; /* текущая запись #define SEQ_2_STAND 2 #define TILE_POTION 10 -/* init_shad_5 (data:0EF2): frame 15, x 55, y 55, direction 0 (вправо), - * curr_col −1 (ещё за левым краем), curr_row 0, action 0 (стоит). */ -static void do_init_shad_5(uint8_t room) +/* Таблицы init_shad_N (data:0EEA/0EF2) — ПЕРВЫЕ СЕМЬ полей Char в том же + * порядке, в каком их копирует оригинал (`memcpy(&Char, source, 7)`): + * 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(); - Char.frame = 0x0F; - Char.x = 0x37; - Char.y = 0x37; - Char.direction = 0; - Char.curr_col = -1; - Char.curr_row = 0; - Char.action = 0; + Char.frame = tbl[0]; + Char.x = tbl[1]; + Char.y = tbl[2]; + Char.direction = (int8_t)tbl[3]; + Char.curr_col = (int8_t)tbl[4]; + Char.curr_row = (int8_t)tbl[5]; + Char.action = tbl[6]; Char.room = room; Char.charid = CHARID_1_SHADOW; 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 { + /* Уровень 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 (room != SHADOW_STEAL_ROOM) return 0; /* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража * комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */ 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; } @@ -604,12 +621,30 @@ static void autocontrol_shadow_level5(void) if (Char.x < 15) clear_char(); } -/* autocontrol_shadow (seg002:081D) — по уровням. Ветки 6 (шагает по плите) - * и 12 появятся со своими уровнями. */ +/* autocontrol_shadow_level6 (seg002:1064). Вся логика — один осторожный шаг + * (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) { if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4(); 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). */ diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 94c91be..6e9dc40 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -94,6 +94,15 @@ #define SHADOW_DOOR_TILEPOS 1 /* (кол 1, ряд 0) — ворота */ #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). На всех * остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см. * pop_check_can_guard_see_kid. */ diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c index 088a18d..c0eb5ed 100644 --- a/applications/PoP/roomtest/pop_level.c +++ b/applications/PoP/roomtest/pop_level.c @@ -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) : 20; /* кромка уровня */ } 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(); diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index 8a57840..1e42cc2 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -274,7 +274,7 @@ static void draw_tile(int row, int col) * «стена» там стоит, только если такой комнаты нет — как в * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ 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), а он * нужен паласной панели над воротами. Ставим 0 = «панели нет»: * случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого