diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index eb86225..d8b2841 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS) +EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 8=roomtest_cold.c $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте @@ -112,4 +112,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py -$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) +$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) diff --git a/applications/PoP/roomtest/NEXT_SESSION.md b/applications/PoP/roomtest/NEXT_SESSION.md index be622bf..1ac6e1e 100644 --- a/applications/PoP/roomtest/NEXT_SESSION.md +++ b/applications/PoP/roomtest/NEXT_SESSION.md @@ -1,4 +1,4 @@ -# Точка входа для следующей сессии (обновлено 2026-08-12) +# Точка входа для следующей сессии (обновлено 2026-08-12, вечер) Файл для старта с чистого контекста: где всё стоит, что делать первым, какие грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам: @@ -6,39 +6,37 @@ --- -## 1. СЛЕДУЮЩАЯ ЗАДАЧА: живая проверка зелий в MAME +## 1. СЛЕДУЮЩАЯ ЗАДАЧА: smoke-прогон уровня 8 -Код зелья медленного падения НАПИСАН (все шесть шагов плана -[`TASKS_OPEN.md#l7-feather`](TASKS_OPEN.md#l7-feather)) и закрыт хост-тестом, -но в MAME ещё не смотрелся. Проверить две сцены: +Зелья приняты пользователем (уровень 2 — «−HP», уровень 7 — перо), уровень 7 +считается прошедшим smoke. Спецсобытие уровня 8 — +[L8-MOUSE](TASKS_CLOSED.md#l8-mouse) — **закрыто 2026-08-12**: мышь +портирована, покрыта хост-набором `t_mouse` (17 проверок) и проверена живьём в +MAME (появилась, нажала кнопку (0,7) комнаты 16, решётка (0,3) поехала вверх, +ушла и погасла). Новых тайлов уровень 8 не приносит вовсе. -- **уровень 7, комната 1** — зелье пера: зелёные пузырьки, зелёная вспышка - при питье, спуск в шахту заметно медленнее обычного и без урона; -- **уровень 2, комната 13, тайл (1,3)** — зелье «−HP»: пузырьки СИНИЕ (были - красные — цвет по типу зелья не был портирован вовсе), HP убавляется на - единицу, экран краснеет ровно один раз. +Осталось пройти сам уровень: он длинный, с возвратом через два чомпера +комнаты 4, и стражи там сильнее прежних — комната 5 держит **skill 7** +(максимум из встречавшихся), комнаты 12/22/24 — skill 2/3/3. -Заодно попадают под проверку `pop_flash_color` (жёлтый/красный/зелёный) и -атлас `pop_pot` (23 → 37 спрайтов: добавлены зелёный и синий наборы кадров -пузырька). +Правило появления мыши (сверено с оригиналом, детали в +[TASKS_CLOSED](TASKS_CLOSED.md#l8-mouse)): счётчик — сам `leveldoor_open`, +растёт только пока Кид в комнате 16, срабатывание ровно на 150, **второго +раза нет**; сбрасывает только рестарт уровня. Тупика это не создаёт — низом +через провал есть путь к двери уровня ценой 1 HP. -**Зацеп в падении — закрыт 2026-08-12** -([GRAB-BELOW-ROOM](BUGS_CLOSED.md#grab-below-room)). Оказалось, что зацеп -из падения был жив, а сломан был спуск через НИЖНЮЮ кромку комнаты (уровень 7, -комната 14): `do_fall` не пускал ряд персонажа до 3, и `check_grab` на этом -отрезке не звался ни разу. Плюс восстановлена ветка `ACT_MIDAIR` (кадры -102..105 — начало падения). Проверено хост-тестом `t_grab` и живьём в MAME. +Резидент разгружен ([MEM-COLD1](TASKS_CLOSED.md#mem-cold1)): холодная половина +главного цикла уехала в **банк 8** (`roomtest_cold.c`), куча 879 → **1707 Б**. +Следующие кандидаты — [MEM-COLD2](TASKS_OPEN.md#mem-cold2). -Осталась ощущенческая часть — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing): -зацеп при входе на уровень 7 срабатывает менее стабильно, чем в оригинале -(подозрение на клавиатурный модуль и на общий темп игры, `L1-SPEED`). -**Решение пользователя: разбирать после того, как будут готовы все уровни.** +Ощущенческий хвост прежний — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing), +разбирается после готовности всех уровней (решение пользователя). ## 2. Состояние репозитория -Всё закоммичено, `main` чист. `tests-host`: 5 наборов проходят -(`[char] 65`, `[geom] 3144`, `[grab] 55`, `[phys] 1727`, `[wall] 1`). -`make size-check`: OK (65 программ, роста нет). +Всё закоммичено, `main` чист. `tests-host`: **6 наборов** проходят +(`[char] 65`, `[geom] 3144`, `[grab] 55`, `[mouse] 17`, `[phys] 1727`, +`[wall] 1`). `make size-check`: OK (65 программ, роста нет). Коммиты 2026-08-12: @@ -55,10 +53,17 @@ ## 3. Состояние окружения - **MAME запущена с мостом**, на HDD — roomtest, собранный с - `-DFIRST_LEVEL=7`. Пересобрать образ: `make hdd` (или - `make PROF_FLAGS="-DPROF_BORDER=1 -DFIRST_LEVEL=N" hdd` — при смене - `FIRST_LEVEL` нужен `touch roomtest.c`, иначе не переедет), после чего - MAME **обязана** пройти полный рестарт (memory `mame_hdd_rebuild_restart`). + `-DFIRST_LEVEL=8`. **ПРАВИЛО ПОЛЬЗОВАТЕЛЯ: перед сборкой образа — + `make clean`, потом `make`, потом `make hdd`** (memory + `mame_build_clean_rule`). Без этого `make hdd` кладёт на диск ПРОШЛУЮ + сборку: `FIRST_LEVEL` живёт внутри `roomtest.c`, и без изменения исходника + make считает `.exe` актуальным — 2026-08-12 на этом уехал уровень 1 вместо + 8, заметил пользователь. После пересборки образа MAME **обязана** пройти + полный рестарт (memory `mame_hdd_rebuild_restart`). +- Адреса состояния сместились после MEM-COLD1: брать из + `.sprinter-cc-roomtest/roomtest.map` заново (`cur_room` 0xA33D, + `pop_leveldoor_open` 0xA34C, `Kid` 0xA3B1, `Guard` 0xA759 — на сборку + 2026-08-12). Мост: `setmem ADDR HEXBYTES` (не `write`), `mem ADDR N`. - **Остановка MAME — `mame/v306/bridge_cmd.sh 'cmd exit'`**, НЕ `kill` (правило пользователя). `pkill -f` бывает не срабатывает, и остаются ДВА инстанса, оба отвечающих на файловый IPC — тогда скриншоты приходят от diff --git a/applications/PoP/roomtest/TASKS_CLOSED.md b/applications/PoP/roomtest/TASKS_CLOSED.md index cd4cb0d..6ab3cb7 100644 --- a/applications/PoP/roomtest/TASKS_CLOSED.md +++ b/applications/PoP/roomtest/TASKS_CLOSED.md @@ -11,6 +11,103 @@ --- +## Уровни: спецсобытия + + +### L8-MOUSE. Мышь уровня 8 — ЗАКРЫТА 2026-08-12 + +Спецсобытие комнаты 16: кнопка (0,7) открывает решётку (0,3), но с левой +площадки верхнего ряда до кнопки не дойти — между ними провал в три колонки. +Когда дверь уровня уже открыта, а Кид всё ещё в комнате 16, оригинал через +150 кадров присылает мышь: она пробегает справа налево, наступает на кнопку и +убегает. Порт по SDLPoP: `do_timers` seg003:0545, `do_mouse` seg003:0ABA, +`autocontrol_mouse` seg002:07EB. + +**Ассетов и отрисовки не потребовалось вовсе.** Кадры 186..188 лежат в +таблице КИДА (`load_frame` seg006:0293 — общий case), их images 130..132 уже +упакованы в `kid16.atl` (idx 2/3/4: 17×4, 19×4, 12×10) — `pop_pack_kid.py` +берёт все `res(401+N).png`, что есть в `data/KID`. Отрисовка подхватывает +мышь сама: `pop_frame_tbl_is_guard(24, …)` отдаёт 0, значит и таблица кадров, +и набор атласов берутся кидовские, и палитра (0x70) тоже. + +Что написано: + +| где | что | +|---|---| +| `guards.c` | `pop_check_mouse` (условие + рождение), `autocontrol_mouse`, ветки мыши в `autocontrol_opponent` / `play_guard` / `pop_check_can_guard_see_kid` | +| `pop_state.c/h` | `pop_leveldoor_open` стал **word**, как в оригинале (data.h:361): в него же считается задержка, байт переполнился бы через 255 кадров и обнулил признак «дверь открыта» | +| `pop_guard.c` | `leave_guard` не записывает в данные уровня тень и мышь (seg002:02F5) — раньше эта проверка отсутствовала вовсе | +| `pop_cdraw.c` | полосы HP у мыши нет (`draw_guard_hp`, seg000:1159) | +| `pop_tune.h` | числа события (уровень/комната/задержка/пороги X) | + +**Правило появления — мышь ОДНОРАЗОВАЯ (сверено с оригиналом).** Счётчик +живёт в `leveldoor_open`: 0 на старте уровня (seg003:97), 1 — когда створка +двери уровня доехала доверху (seg007:456, то есть Кид нажал кнопку в комнате +3), дальше `++` каждый кадр, пока Кид в комнате 16, и вызов ровно при +`== 150`. Отсюда три следствия: + +- второго раза НЕТ: счётчик растёт дальше, а сравнение строгое. Не успел + пройти решётку, пока она открыта — мышь больше не придёт; +- выход из комнаты 16 счётчик не сбрасывает, а ЗАМОРАЖИВАЕТ: ушёл на сотом + кадре, вернулся — ждать осталось пятьдесят; +- обнуляет только `start_level`, то есть смерть/рестарт уровня — но он же + возвращает тайлы и модификаторы, так что дверь придётся открывать заново. + +Значения `0x4D` (музыка уровней 4 и 6) и `2` (победа над Джаффаром, уровень +13) в ту же переменную к уровню 8 отношения не имеют — там счёт идёт +монотонно 1 → 150. + +**Чего в условии НЕТ (проверено грепом по всем упоминаниям `mouse_level` / +`mouse_room` / `do_mouse` — единственная точка запуска это seg003:545):** +оригинал не смотрит ни на состояние решётки (0,3), ни на то, с какой стороны +провала стоит Кид, ни на то, прошёл ли он её уже. «Кид заперт за закрытой +решёткой» получается САМО: находиться в комнате 16 двенадцать с половиной +секунд можно только топчась там, а стоит уйти в комнату 4 — счётчик +замирает. Обратная сторона той же простоты: вернувшись в комнату 16 с ЛЮБОЙ +стороны, игрок доигрывает накопленное ожидание и может получить мышь, даже +если решётка в этот момент открыта. + +Тупика при этом нет: провал в колонках 4..6 ведёт на нижний ряд комнаты +(`debris` между колоннами, а колонна — проходимый пол, seg006:0628), оттуда +низом через комнату 4 с шипами есть путь к двери уровня в комнате 3. Цена — +падение на два ряда, то есть −1 HP. Мышь экономит эту единицу, а не спасает +от тупика. + +**Тонкость, на которой легко сломать уход:** `seq_107` прыгает на метку +`Mscurry1`, стоящую ПОСЛЕ опкода `act(actions_1_run_jump)` в `seq_105` — на +обратном пути кадры бега проигрываются, а `action` остаётся стоечным. Именно +этим оригинал отличает «бежит к кнопке» от «убегает»: условие исчезновения +висит на `action == 0`. + +**Проверено.** Хост-набор `t_mouse` (17 проверок, единственный, куда +линкуется `guards.c`) — рождение по таймеру, замороженный счётчик вне комнаты, +требование открытой двери, нажатие кнопки на пробеге, разворот и исчезновение, +невидимость для Кида, необращение в «стража комнаты». Регресс проверен +откатом: без ветки в `autocontrol_opponent` падают две проверки ухода. +Живьём в MAME (комната 16 уровня 8, `leveldoor_open` взведён через мост): +мышь появилась (`charid = 0x18`), добежала до колонки 7 (кадр 188, x = 164), +решётка (0,3) поехала вверх, мышь ушла вправо и погасла (`direction = 0x56`). + + +### MEM-COLD1. Холодная половина главного цикла — в банк 8 (2026-08-12) + +К уровню 8 куча в режиме huge упала до **879 Б**. Убирать из `roomtest.c` +оказалось нечего — отладочное там всё живое (обход комнат, стоп-кадр, читы, +профиль), — поэтому вынесли в банк то, что отрабатывает раз на уровень или +только по клавише: `pop_start_level` + `find_start_level_door`, чит-навигацию +по комнатам и лейбл номера комнаты (`roomtest_cold.c`, `--bank 8`, `n_banks` +поднят до 8). + +Результат: `_CODE` 25653 → 24825, **куча 879 → 1707 Б**, банк 8 занят на 5 %. +Кадровый путь не тронут: за обычный кадр в банк 8 уходит ноль вызовов, при +смене комнаты — один трамплин. + +Что осталось в резиденте намеренно: `enter_room_side` и рабочие массивы +комнаты (их читает и главный цикл, и холодная половина), `guard_over_kid` с +хелперами (зовётся каждый кадр). Следующий шаг — [MEM-COLD2](TASKS_OPEN.md#mem-cold2). + +--- + ## Приёмка уровней diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 3f8c6de..b08eef1 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -35,7 +35,38 @@ --- -## ТЕКУЩАЯ ЦЕЛЬ: уровень 5 +## ТЕКУЩАЯ ЦЕЛЬ: уровень 8 + +Уровни 1-7 играются (smoke; зелья уровней 2 и 7 приняты пользователем +2026-08-12). Уровень 8 **новых тайлов не приносит вовсе** (инвентарь +`res2008.bin` целиком покрыт уровнями 1-7), тайлсет — подземелье. +Спецсобытие уровня — мышь, [L8-MOUSE](TASKS_CLOSED.md#l8-mouse) закрыта +2026-08-12 (хост-набор `t_mouse` + живая проверка в MAME). + +Остаётся smoke-прогон самого уровня: он длинный и с возвратом через два +чомпера комнаты 4, а стражи там сильнее прежних (комната 5 — skill **7**, +это максимум из встречавшихся; 12/22/24 — skill 2/3/3). + + +### MEM-COLD2. Второй шаг разгрузки резидента + +После [MEM-COLD1](TASKS_CLOSED.md#mem-cold1) куча 1707 Б. Когда упрётся +снова, в банк 8 переезжают следующие кандидаты (в порядке выгоды): + +1. `enter_room_side` — самая большая функция `roomtest.c`, зовётся раз на + комнату. Требует вынести наружу рабочие массивы комнаты + (`room_bg`/`lcol_*`/`rcol_*`/`below_fg`/`above_*`) — сами данные останутся + в `_DATA`, уедет только код; +2. `guard_over_kid` + `objtile_at_char`/`char_x_left_of`/`tile_div_mod` — + зовётся КАЖДЫЙ кадр, но ровно один раз: цена выноса — один трамплин; +3. блок редроя левого шва в `main` (`seam_row_sig` + разбор сигнатуры). + +Если и этого не хватит — следующим уходит сам кадровый порядок отрисовки, +но тогда уже стоит мерить, а не выносить наугад. + +--- + +## ПРЕДЫДУЩАЯ ЦЕЛЬ: уровень 5 Уровни 1-4 играются (smoke). Дальше идём по порядку уровней; уровень 5 — следующий. diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index d2dd3e5..ee35b36 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -98,6 +98,10 @@ void pop_check_can_guard_see_kid(void) __banked int16_t left_pos, right_pos; uint8_t tile; + /* МЫШЬ Кида «не видит» (seg003:0965): иначе он выхватит меч на зверька, + * пробегающего мимо. */ + if (Guard.charid == CHARID_24_MOUSE) { can_guard_see_kid = 0; return; } + /* Первый множитель — порт seg003:702. ТЕНЬ боевая только на 12-м * уровне; на 4/5/6 она Кида «не видит», и это не косметика: увидев её, * Кид достаёт меч (control_standing, seg005:352), а сама тень идёт той @@ -198,6 +202,50 @@ void pop_check_skel(void) __banked is_guard_notice = guard_refrac = 0; } +/* ---- Мышь: спецсобытие уровня 8 (do_timers seg003:0545 + do_mouse) --- * + * + * Кнопка (0,7) комнаты 16 открывает решётку (0,3), но с левой площадки + * верхнего ряда до кнопки не дойти — между ними провал в три колонки. + * Поэтому, когда дверь уровня уже открыта, а Кид всё ещё в комнате 16, + * оригинал через POP_MOUSE_DELAY кадров присылает мышь: она пробегает + * справа налево, наступает на кнопку (кнопку жмёт ОБЩИЙ check_press из + * физики соперника, как и любой персонаж) и убегает обратно. + * + * Счётчик — сам pop_leveldoor_open, как в оригинале: он растёт ТОЛЬКО пока + * Кид в комнате 16, поэтому «12,5 секунды» отсчитываются с момента, когда + * игрок туда вернулся, а не с открытия двери. Ради этого он и word + * (см. pop_state.c). + * + * Слот соперника занимаем БЕЗ проверки «свободен ли» — так в оригинале, и + * на уровне 8 конфликта нет: в данных комнаты 16 стража нет + * (guards_tile[15] = 30 = «пусто»). */ +#define SEQ_105_MOUSE_FORWARD 105 +#define SEQ_107_MOUSE_STAND_UP_GO 107 + +void pop_check_mouse(void) __banked +{ + if (pop_current_level != POP_MOUSE_LEVEL) return; + if (Kid.room != POP_MOUSE_ROOM) return; + if (!pop_leveldoor_open) return; + if (++pop_leveldoor_open != POP_MOUSE_DELAY) return; + + /* do_mouse (seg003:0ABA) поднимает мышь В ОКНЕ Char, начиная с КИДА + * (loadkid): поля, которых нет в списке ниже — frame/action/room/ + * fall_x/fall_y — наследуются от него. Комната как раз оттуда и + * берётся, отдельного присваивания в оригинале нет. */ + pop_loadkid(); + Char.charid = CHARID_24_MOUSE; + Char.x = POP_MOUSE_START_X; + Char.curr_row = 0; + Char.y = (uint8_t)pop_y_land[1]; + Char.alive = -1; + Char.direction = -1; /* dir_FF_left */ + guardhp_curr = 1; + pop_char_set_seq(SEQ_105_MOUSE_FORWARD); + play_seq(); + pop_saveshad(); +} + /* ---- Тень: спецсобытие входа в комнату (check_shadow, seg002:0064) -- * * * На «шадоу»-уровнях слот соперника занимает ТЕНЬ, и занимает его ВМЕСТО @@ -638,6 +686,26 @@ static void autocontrol_shadow_level6(void) move_1_forward(); } +/* autocontrol_mouse (seg002:07EB). Весь «ИИ» мыши — два порога по X: + * добежала влево до POP_MOUSE_TURN_X → seq_107 (встать, поднять голову, + * развернуться и уйти), вернулась вправо до POP_MOUSE_LEAVE_X → исчезнуть. + * + * Почему проверка на исчезновение висит на action == 0 (стойка), хотя мышь + * при этом БЕЖИТ: seq_107 заканчивается прыжком на метку `Mscurry1`, которая + * стоит ПОСЛЕ опкода act(actions_1_run_jump) в seq_105 — то есть на обратном + * пути кадры бега проигрываются, а action остаётся стоечным. Именно этим + * оригинал и различает «бежит туда» и «бежит обратно». */ +static void autocontrol_mouse(void) +{ + if (Char.direction == DIR_56_NONE) return; + if (Char.action == 0) { + if (Char.x >= POP_MOUSE_LEAVE_X) clear_char(); + } else if (Char.x < POP_MOUSE_TURN_X) { + pop_char_set_seq(SEQ_107_MOUSE_STAND_UP_GO); + play_seq(); + } +} + /* autocontrol_shadow (seg002:081D) — по уровням. Ветка 12 появится со своим * уровнем. */ static void autocontrol_shadow(void) @@ -658,6 +726,8 @@ static void autocontrol_opponent(void) /* autocontrol_skeleton (seg002:685): у скелета меч вынут ВСЕГДА — он не * бывает «неактивным», поэтому ветка inactive ему недоступна. */ if (Char.charid == CHARID_4_SKELETON) Char.sword = SWORD_2_DRAWN; + /* МЫШЬ — своей веткой (seg002:0643), первой в цепочке charid'ов. */ + if (Char.charid == CHARID_24_MOUSE) { autocontrol_mouse(); return; } /* ТЕНЬ идёт своей веткой ЦЕЛИКОМ (seg002:0647): к стражьему ИИ она не * сводится — на уровне 4 не дерётся вовсе. */ if (Char.charid == CHARID_1_SHADOW) { autocontrol_shadow(); return; } @@ -780,6 +850,11 @@ void pop_check_sword_hurt(void) __banked * доигрывает последовательность. */ static void play_guard(void) { + /* МЫШЬ (seg006:0D85, первая ветка): ей положен ТОЛЬКО автоконтроль. + * Диспетчер control() ей не нужен и вреден — он бы гонял её по + * стоечным веткам управления, а вся её «жизнь» уже описана + * последовательностями seq_105/107. */ + if (Char.charid == CHARID_24_MOUSE) { autocontrol_opponent(); return; } if (Char.alive < 0 && guardhp_curr == 0) { Char.alive = 0; /* on_guard_killed (seg006:1927) — у нас пока только звук победы, diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 98a2186..a4c8af6 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -807,8 +807,10 @@ void pop_hp_draw(void) __banked for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++) pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF); - /* Страж: только полные деления, справа налево. Скелет полосы не имеет. */ - if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) { + /* Страж: только полные деления, справа налево. Скелет полосы не имеет, + * мышь (спецсобытие уровня 8) — тоже: draw_guard_hp, seg000:1159. */ + if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && + Guard.charid != CHARID_24_MOUSE && gd) { const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0); uint16_t w, h; gfx_w0_map(gp[0].page); diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index d638594..1af736f 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -185,6 +185,10 @@ void pop_guard_load_frame(void) void pop_guard_leave(void) { if (Guard.charid == 0) return; /* стража не было */ + /* Тень и МЫШЬ в данные уровня не записываются (seg002:02F5): они не + * «стражи комнаты», а спецсобытия — вернувшись, их надо получить заново + * от check_shadow / check_mouse, а не поднять из guards_tile. */ + if (Guard.charid == CHARID_1_SHADOW || Guard.charid == CHARID_24_MOUSE) return; pop_guard_state_save(Guard.room, (uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */ Guard.direction, Guard.x, guard_skill, diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index e3c3fe0..0ac1d57 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -68,6 +68,12 @@ #define CHARID_2_GUARD 2 #define CHARID_4_SKELETON 4 #define CHARID_5_PRINCESS 5 +/* МЫШЬ (спецсобытие уровня 8) — тоже персонаж слота Guard: своей таблицы + * кадров у неё нет, кадры 186..188 лежат в таблице КИДА (load_frame, + * seg006:0293 — общий case с charid_0_kid), а спрайты (images 130..132) — + * в атласах Кида, страница kid16.atl. Поэтому отрисовка её подхватывает + * без единой правки: pop_frame_tbl_is_guard(24,…) отдаёт 0. */ +#define CHARID_24_MOUSE 24 /* dir_56_none (types.h:402): «направления нет» — им clear_char убирает * персонажа со сцены. Значение 0x56 в int8_t = 86, то есть заведомо не @@ -191,6 +197,12 @@ void pop_check_can_guard_see_kid(void) __banked; * в кадр, ДО логики персонажей (порядок play_frame, seg000:868). */ void pop_check_skel(void) __banked; +/* Спецсобытие уровня 8 — МЫШЬ (do_timers seg003:0545 + do_mouse seg003:0ABA). + * Условие запуска: дверь уровня открыта И Кид в комнате 16; тогда счётчик + * pop_leveldoor_open растёт кадрами, и на 150-м мышь появляется у правого + * края верхнего ряда. Звать раз в кадр рядом с pop_check_skel. */ +void pop_check_mouse(void) __banked; + /* check_shadow (seg002:0064) — СПЕЦСОБЫТИЕ входа в комнату: на «шадоу»- * уровнях слот соперника занимает ТЕНЬ, а не страж из данных. Звать ВМЕСТО * pop_guard_enter при входе в комнату; вернула 1 — событие обработано и diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index 75f1680..615f83b 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -27,8 +27,13 @@ uint8_t pop_droppedout; /* leveldoor_open (seg007:456): дверь уровня доехала до верха. Ставит * анимация двери (pop_trob, банк 6), читает спецсобытие подъёма скелета * (guards.c, банк 1) — снова случай «данные через границу банков». - * Сбрасывается стартом уровня (seg003:97). */ -uint8_t pop_leveldoor_open; + * Сбрасывается стартом уровня (seg003:97). + * + * ШИРИНА — word, как в оригинале (data.h:361), а не byte: на уровне 8 + * do_timers СЧИТАЕТ этой же переменной время до появления мыши + * (`++leveldoor_open` каждый кадр, пока Кид в комнате 16), и байт + * переполнился бы через 255 кадров, обнулив признак «дверь открыта». */ +uint16_t pop_leveldoor_open; /* ---- Шина управления (seg006 globals) ------------------------------ * * Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка — diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h index 6347e4e..29e3ff1 100644 --- a/applications/PoP/roomtest/pop_state.h +++ b/applications/PoP/roomtest/pop_state.h @@ -43,8 +43,10 @@ extern int pop_leveldoor_ybottom; extern uint8_t pop_droppedout; /* leveldoor_open (seg007:456): 1 — дверь уровня открыта полностью. Пишет - * анимация двери (банк 6), читает подъём скелета (банк 1). */ -extern uint8_t pop_leveldoor_open; + * анимация двери (банк 6), читает подъём скелета (банк 1). Word, как в + * оригинале: на уровне 8 в него же СЧИТАЕТСЯ задержка до мыши (см. + * pop_state.c и pop_check_mouse в guards.c). */ +extern uint16_t pop_leveldoor_open; /* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c. * Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h index 57e9a5d..a454396 100644 --- a/applications/PoP/roomtest/pop_tune.h +++ b/applications/PoP/roomtest/pop_tune.h @@ -90,6 +90,25 @@ * (TASKS_OPEN.md, L1-SPEED), а не свойство зелья. */ #define POP_FEATHER_FRAMES 225 +/* ---- Спецсобытие «МЫШЬ» (уровень 8, комната 16) ---------------------- * + * Кнопка (0,7) комнаты 16 открывает решётку (0,3) — ту, за которой лежит + * путь через комнату 4 (два чомпера) к кнопке выхода в комнате 3. Сам Кид + * до кнопки с левой площадки не дотягивается (провал в колонках 4..6), + * поэтому, когда дверь уровня уже открыта, а он всё ещё в комнате 16, + * оригинал через 150 тиков присылает мышь: она пробегает справа налево по + * верхнему ряду, наступает на кнопку и убегает обратно (do_timers + * seg003:0545, do_mouse seg003:0ABA, autocontrol_mouse seg002:07EB). + * + * Числа — custom-константы SDLPoP (data.h:821): уровень/комната/задержка/ + * стартовая X. Пороги разворота (166) и исчезновения (200) — литералы + * autocontrol_mouse. Задержка в ИГРОВЫХ КАДРАХ, как POP_FEATHER_FRAMES. */ +#define POP_MOUSE_LEVEL 8 +#define POP_MOUSE_ROOM 16 +#define POP_MOUSE_DELAY 150 +#define POP_MOUSE_START_X 200 +#define POP_MOUSE_TURN_X 166 /* x < этого — развернуться и уйти */ +#define POP_MOUSE_LEAVE_X 200 /* x >= этого в стойке — исчезнуть */ + /* ---- Уровень зелий (SDLPoP: «potions level», seg006:1894) ------------ * * На нём синие склянки «вреда» забирают ПОЛОВИНУ запаса HP, а не единицу. * В оригинале это условие `current_level == 15` прямо в proc_get_object. */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index c9672fb..89186d8 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -21,6 +21,7 @@ #include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */ #include "pop_cheat.h" #include "pop_tune.h" +#include "roomtest_cold.h" /* холодная половина цикла — в банке */ __sfr __at (0xFE) io_border; @@ -54,7 +55,9 @@ __sfr __at (0xFE) io_border; * оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw * (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления), * 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина - * фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты). + * фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты), + * 8 = roomtest_cold (ХОЛОДНАЯ половина главного цикла: старт уровня и + * отладочная навигация — см. roomtest_cold.h). * Отрисовка персонажей и холодная половина фона вынесены из банка 2 * отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть * куда — тайлы поздних уровней, чомперы, зеркало. @@ -64,7 +67,7 @@ __sfr __at (0xFE) io_border; * останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку * runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра * доходила до конца загрузки ресурсов и вставала намертво в DSS. */ -const uint8_t n_banks = 7; +const uint8_t n_banks = 8; /* Режим читов (pop_cheat.h): на время разработки включаем в main. */ uint8_t pop_cheats; @@ -119,13 +122,14 @@ static void kbd_idle(void) /* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы * соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * чтобы enter_room переключал комнату без длинного списка параметров. */ -static uint8_t room_fg[30], room_bg[30]; +uint8_t room_fg[30]; +static uint8_t room_bg[30]; /* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0), * [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и * смотрит наружу (колонка −2 / 11). См. pop_level.h. */ static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11]; static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */ -static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */ +uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */ static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */ /* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid @@ -258,10 +262,11 @@ static uint8_t seam_sig[3]; /* последняя нарисованная ope static uint8_t seam_rows; /* маска рядов, ждущих редроя (переживает 2 кадра) */ static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */ -/* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая): - * sig = последняя показанная комната, redraw = остаток кадров (2 = обе страницы). */ -static uint8_t room_label_sig = 0xFF; -static uint8_t room_label_redraw; +/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7 + * уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет + * стартовую позицию. Сбрасывается началом игры и прохождением уровня — + * ровно как в оригинале (seg003:37 и seg003:413). */ +uint8_t pop_checkpoint; /* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления * куска, поднятый предмет → пол) пишутся ПРЯМО В УРОВЕНЬ — @@ -387,7 +392,7 @@ static void enter_room_side(uint8_t room, uint8_t side) /* Вход в комнату БЕЗ перехода за Кидом: старт уровня, респавн, чит-обход. * Страж такой смены не «видит» — он остаётся в своей комнате. */ -static void enter_room(uint8_t room) { enter_room_side(room, 0xFF); } +void enter_room(uint8_t room) { enter_room_side(room, 0xFF); } /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos * (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и @@ -414,109 +419,6 @@ static void enter_room(uint8_t room) { enter_room_side(room, 0xFF); } #define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */ #define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */ -/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ — - * это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью - * (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72). - * Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки: - * его стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина - * той самой двери. */ -static void find_start_level_door(uint8_t room) -{ - uint8_t *m = pop_trob_modif(room); - uint8_t tp; - if (!m) return; - /* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз - * за уровень, а pop_add_trob сам лезет в W0. */ - for (tp = 0; tp < 30; tp++) { - if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue; - m[tp] = 43; /* створка поднята полностью */ - pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */ - } -} - -/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7 - * уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет - * стартовую позицию. Сбрасывается началом игры и прохождением уровня — - * ровно как в оригинале (seg003:37 и seg003:413). */ -static uint8_t pop_checkpoint; - -static void pop_start_level(void) -{ - pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ - pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */ - uint8_t entry_pose = pop_level_entry_pose(pop_current_level); - uint8_t start_room = pop_level_start_room(); - uint8_t pos = pop_level_start_pos(); - int8_t dir = (int8_t)~pop_level_start_dir(); - uint8_t seq = SEQ_TURN; - /* Особое событие: старт с чекпойнта (do_startpos, seg003:0141). - * Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles() - * ниже, иначе рестарт её вернёт. */ - uint8_t chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint); - if (chkp) { - start_room = POP_CHKP_RESPAWN_ROOM; - pos = POP_CHKP_RESPAWN_TILEPOS; - dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную - * и разворачивается seq_5 */ - } - - /* Рестарт уровня = load_level() заново (play_level, seg003:57): в - * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы — - * разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и - * убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */ - pop_trob_reset(); - pop_level_reset_tiles(); - pop_level_reset_guards(); - pop_guard_reset(); /* start_level: Guard.direction = none */ - pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ - /* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в - * комнату-чекпойнт после респавна остался бы непройденным. */ - if (chkp) - pop_level_set_tile(POP_CHKP_CLEAR_ROOM, - (uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL), - TILE_EMPTY); - - /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos - * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы - * ещё не тронутое состояние решётки. */ - if (entry_pose == ENTRY_POSE_FALL) { - /* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5, - * col2, row0 её же типом и модификатором (в оригинале и то и другое - * берётся из curr_* после get_tile). */ - uint8_t tp = 2; /* row0*10 + col2 */ - pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]); - seq = SEQ_FALL; - } else if (entry_pose == ENTRY_POSE_RUN) { - seq = SEQ_RUN; - } - - /* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть - * открыта уже в первом кадре (в оригинале find_start_level_door стоит - * между do_startpos и draw_level_first). */ - find_start_level_door(start_room); - - enter_room(start_room); - kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir); - /* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м - * уровне Кид ставится в комнату 17, а экран тут же переводится на - * комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) — - * он влетает в неё сверху, продолжая падение с шестого уровня. */ - if (pop_current_level == FALLING_ENTRY_LEVEL && - start_room == FALLING_ENTRY_ROOM) { - uint8_t below = pop_room_link(start_room, LINK_DOWN); - if (below) { - Kid.y = (uint8_t)(Kid.y - 189); - Kid.curr_row = pop_y_to_row((int16_t)Kid.y); - enter_room(below); - } - } - Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ - /* play_level (seg003:106): have_sword = level >= have_sword_from_level - * (=2) — начиная со второго уровня меч у Кида уже есть. */ - pop_have_sword = (uint8_t)(pop_current_level >= 2); - pop_kid_hp_reset(); -} - int main(void) { /* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1 @@ -663,89 +565,13 @@ int main(void) #ifdef ROOMNAV /* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд, * '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна - * комната. - * - * Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната - * смотрелась в исходном состоянии». Убран: reset обнуляет room_seen, - * и модификаторы перечитываются из уровня заново, то есть чит - * ОТМАТЫВАЕТ МИР — открытые/закрытые ворота, выдвинутые пики, нажатые - * кнопки. Пока ворота с bg=1 ошибочно стартовали закрытыми, откат был - * не виден; после порта load_alter_mod он выглядит как баг «ворота - * снова открылись» (уровень 1, решётка комнаты 5 (0,9), которую - * закрывает стартовый триггер do_startpos). Навигация обязана только - * телепортировать; исходное состояние даёт перезапуск уровня. */ + * комната. Сама навигация — в банке (roomtest_cold.c): она нужна + * только по клавише, а место в резиденте дефицитно. */ { uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) | ((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1)); - if (nav && !nav_prev) { - 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) - : (uint8_t)(r == 1 ? 24 : r - 1); - enter_room(r); - /* Куда поставить Кида в новой комнате: пол С МАКСИМАЛЬНЫМ Y — - * сначала НИЖНИЙ ряд (2), потом 1, и только потом 0 - * (пользователь 2026-08-11). Раньше брался первый пол - * подряд, то есть почти всегда (0,0) — верхний левый угол: - * это и «прилёт» в проём/ворота, и, на уровне 5, посадка - * вплотную к тени спецсобытия, которая ждёт в колонке −1 - * ряда 0. Вместо сцены кражи зелья начиналась схватка, - * которой в этой комнате быть не должно. Снизу вверх нужно - * и самой навигации: в верхнем ряду пола чаще нет, и Кид - * проваливается из комнаты сразу после телепорта. - * - * ИСКЛЮЧЕНИЕ — комната 14 уровня 7 (пользователь 2026-08-12): - * её проходят СВЕРХУ ВНИЗ (с ряда 0 на ряд 2 через зацеп за - * кромки), и с нижнего ряда этот спуск не проверить вовсе. - * Пол в ряду 0 там есть, так что Кид не проваливается. - * - * В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его - * там нет — любой проходимый tile_is_floor (seg001: всё, - * кроме пустоты, верха колонны, верха двери, стены и - * «половинок» 26..29). */ - { - int8_t row, step = -1, first = 2; - if (pop_current_level == GRAB_DOWN_LEVEL && - r == GRAB_DOWN_ROOM) { first = 0; step = 1; } - for (row = first; row >= 0 && row <= 2 && any == 0xFF; - row = (int8_t)(row + step)) { - uint8_t fallback = 0xFF; - for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) { - c = (uint8_t)(room_fg[i] & 0x1F); - if (c == 1) { any = i; break; } - if (fallback == 0xFF && c && c != 9 && c != 12 && - c != 20 && !(c >= 26 && c <= 29)) fallback = i; - } - if (any == 0xFF) any = fallback; - } - } - if (any != 0xFF) pos = any; - kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0); - pop_kid_hp_reset(); + if (nav && !nav_prev && pop_dbg_roomnav((uint8_t)(nav & 1 ? 1 : 2))) dead_frames = 0; - /* ВЫХОД ИЗ БОЯ (BUG-CHEAT-FIGHT-1). Телепорт с вынутым мечом - * заклинивал управление намертво: pop_control при - * Kid.sword == SWORD_2_DRAWN уходит в control_with_sword, а - * там единственный выход — ветка «убрать меч» на кадре - * FRAME_171_STAND_WITH_SWORD. После kid_init(SEQ_STAND) кадр - * обычный, стража в новой комнате нет, swordfight не - * запускается — и диспетчер каждый кадр не делает НИЧЕГО. - * Это болезнь ЧИТА: в оригинале телепорта между комнатами - * нет, попасть в режим боя без соперника нечем. Меч в - * инвентаре (pop_have_sword) НЕ теряем — только состояние - * схватки. */ - Kid.sword = SWORD_0_SHEATHED; - holding_sword = 0; - offguard = 0; - guard_refrac = 0; - } nav_prev = nav; } #endif @@ -755,6 +581,7 @@ int main(void) gfx_set_draw_page(back); pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ + pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_ctrl_tick(); /* ввод -> control(): смена seq */ /* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы — @@ -1045,26 +872,7 @@ int main(void) * при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера), * а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */ #ifdef ROOMNAV - { - uint8_t r = cur_room, prev = room_label_sig; - room_label_sig = r; /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */ - if (r != prev) room_label_redraw = 2; - if (room_label_redraw) { - /* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный - * знакогенератор (_gfx_font_buf, 2 КБ статики в W2) и сажает - * кучу с 3867 до 576 Б. Слева десятки, справа единицы: - * || |||| = 24. bar/setfillstyle уже используются. */ - uint8_t i; - setfillstyle(SOLID_FILL, BLACK); - bar(100, 0, 175, 5); - /* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная - * (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */ - setfillstyle(SOLID_FILL, 0x57); - for (i = 0; i < (uint8_t)(r / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5); - for (i = 0; i < (uint8_t)(r % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5); - room_label_redraw--; - } - } + pop_dbg_room_label(cur_room); /* номер комнаты палочками — в банке */ #endif gfx_wait_vsync(); diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c new file mode 100644 index 0000000..7f77ae6 --- /dev/null +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -0,0 +1,249 @@ +/* + * roomtest_cold.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК + * (--bank 8=roomtest_cold.c, окно W3). Здесь всё, что отрабатывает раз на + * уровень или только по отладочной клавише; кадровый путь остался в + * roomtest.c. Зачем разделили и что осталось снаружи — см. roomtest_cold.h. + * + * Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого + * модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие + * __banked-модули через трамплины. + */ +#include +#include +#include "roomtest_cold.h" +#include "pop_bg.h" /* pop_loose_mob_reset — банк 7, зовём через трамплин */ +#include "pop_kid.h" +#include "pop_map.h" +#include "pop_level.h" +#include "pop_trob.h" +#include "pop_guard.h" +#include "pop_state.h" +#include "pop_geom.h" +#include "pop_tune.h" + +#define SEQ_STAND 2 +#define SEQ_TURN 5 +#define SEQ_FALL 7 +#define SEQ_RUN 84 +#define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */ +#define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */ +#define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */ +#define TILE_EMPTY 0 +#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ + +/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ — + * это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью + * (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72). + * Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки: + * его стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина + * той самой двери. */ +static void find_start_level_door(uint8_t room) +{ + uint8_t *m = pop_trob_modif(room); + uint8_t tp; + if (!m) return; + /* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз + * за уровень, а pop_add_trob сам лезет в W0. */ + for (tp = 0; tp < 30; tp++) { + if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue; + m[tp] = 43; /* створка поднята полностью */ + pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */ + } +} + +/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos + * (seg003:028A). + * + * Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define: + * start_pos — тайл 0..29 (row*10+col), start_dir — направление, в котором + * персонаж должен ОКАЗАТЬСЯ. + * + * Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж + * ставится спиной и доворачивается кадрами позы входа. Сама поза — из + * tbl_entry_pose (data.h:848 = {0,1,0,…,2,…}): + * 1 (уровень 1) — падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2); + * это оно захлопывает решётку за спиной в первой комнате; + * 2 (уровень 13) — вбегание (seq_84_run); + * иначе — разворот на месте (seq_5_turn). + * Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */ +void pop_start_level(void) __banked +{ + uint8_t entry_pose, start_room, pos, seq = SEQ_TURN, chkp; + int8_t dir; + + pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ + pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */ + entry_pose = pop_level_entry_pose(pop_current_level); + start_room = pop_level_start_room(); + pos = pop_level_start_pos(); + dir = (int8_t)~pop_level_start_dir(); + /* Особое событие: старт с чекпойнта (do_startpos, seg003:0141). + * Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles() + * ниже, иначе рестарт её вернёт. */ + chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint); + if (chkp) { + start_room = POP_CHKP_RESPAWN_ROOM; + pos = POP_CHKP_RESPAWN_TILEPOS; + dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную + * и разворачивается seq_5 */ + } + + /* Рестарт уровня = load_level() заново (play_level, seg003:57): в + * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы — + * разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и + * убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */ + pop_trob_reset(); + pop_level_reset_tiles(); + pop_level_reset_guards(); + pop_guard_reset(); /* start_level: Guard.direction = none */ + pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ + /* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в + * комнату-чекпойнт после респавна остался бы непройденным. */ + if (chkp) + pop_level_set_tile(POP_CHKP_CLEAR_ROOM, + (uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL), + TILE_EMPTY); + + /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos + * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы + * ещё не тронутое состояние решётки. */ + if (entry_pose == ENTRY_POSE_FALL) { + /* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5, + * col2, row0 её же типом и модификатором (в оригинале и то и другое + * берётся из curr_* после get_tile). */ + uint8_t tp = 2; /* row0*10 + col2 */ + pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]); + seq = SEQ_FALL; + } else if (entry_pose == ENTRY_POSE_RUN) { + seq = SEQ_RUN; + } + + /* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть + * открыта уже в первом кадре (в оригинале find_start_level_door стоит + * между do_startpos и draw_level_first). */ + find_start_level_door(start_room); + + enter_room(start_room); + kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir); + /* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м + * уровне Кид ставится в комнату 17, а экран тут же переводится на + * комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) — + * он влетает в неё сверху, продолжая падение с шестого уровня. */ + if (pop_current_level == FALLING_ENTRY_LEVEL && + start_room == FALLING_ENTRY_ROOM) { + uint8_t below = pop_room_link(start_room, LINK_DOWN); + if (below) { + Kid.y = (uint8_t)(Kid.y - 189); + Kid.curr_row = pop_y_to_row((int16_t)Kid.y); + enter_room(below); + } + } + Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ + /* play_level (seg003:106): have_sword = level >= have_sword_from_level + * (=2) — начиная со второго уровня меч у Кида уже есть. */ + pop_have_sword = (uint8_t)(pop_current_level >= 2); + pop_kid_hp_reset(); +} + +/* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- * + * + * Нужен, чтобы пройти все комнаты уровня и собрать список багов отрисовки, + * включая недостижимые обычным путём. + * + * Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната + * смотрелась в исходном состоянии». Убран: reset обнуляет room_seen, и + * модификаторы перечитываются из уровня заново, то есть чит ОТМАТЫВАЕТ МИР + * — открытые/закрытые ворота, выдвинутые пики, нажатые кнопки. Навигация + * обязана только телепортировать; исходное состояние даёт перезапуск. */ +uint8_t pop_dbg_roomnav(uint8_t dir) __banked +{ + uint8_t r, 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(); + + r = cur_room; + r = (dir == 1) ? (uint8_t)(r == 24 ? 1 : r + 1) + : (uint8_t)(r == 1 ? 24 : r - 1); + enter_room(r); + + /* Куда поставить Кида в новой комнате: пол С МАКСИМАЛЬНЫМ Y — сначала + * НИЖНИЙ ряд (2), потом 1, и только потом 0 (пользователь 2026-08-11). + * Раньше брался первый пол подряд, то есть почти всегда (0,0) — верхний + * левый угол: это и «прилёт» в проём/ворота, и, на уровне 5, посадка + * вплотную к тени спецсобытия, которая ждёт в колонке −1 ряда 0. Вместо + * сцены кражи зелья начиналась схватка, которой в этой комнате быть не + * должно. Снизу вверх нужно и самой навигации: в верхнем ряду пола чаще + * нет, и Кид проваливается из комнаты сразу после телепорта. + * + * ИСКЛЮЧЕНИЕ — комната 14 уровня 7 (пользователь 2026-08-12): её проходят + * СВЕРХУ ВНИЗ (с ряда 0 на ряд 2 через зацеп за кромки), и с нижнего ряда + * этот спуск не проверить вовсе. Пол в ряду 0 там есть, так что Кид не + * проваливается. + * + * В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его там нет — + * любой проходимый tile_is_floor (seg001: всё, кроме пустоты, верха + * колонны, верха двери, стены и «половинок» 26..29). */ + { + int8_t row, step = -1, first = 2; + if (pop_current_level == GRAB_DOWN_LEVEL && + r == GRAB_DOWN_ROOM) { first = 0; step = 1; } + for (row = first; row >= 0 && row <= 2 && any == 0xFF; + row = (int8_t)(row + step)) { + uint8_t fallback = 0xFF; + for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) { + c = (uint8_t)(room_fg[i] & 0x1F); + if (c == 1) { any = i; break; } + if (fallback == 0xFF && c && c != 9 && c != 12 && + c != 20 && !(c >= 26 && c <= 29)) fallback = i; + } + if (any == 0xFF) any = fallback; + } + } + if (any != 0xFF) pos = any; + kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0); + pop_kid_hp_reset(); + + /* ВЫХОД ИЗ БОЯ (BUG-CHEAT-FIGHT-1). Телепорт с вынутым мечом заклинивал + * управление намертво: pop_control при Kid.sword == SWORD_2_DRAWN уходит + * в control_with_sword, а там единственный выход — ветка «убрать меч» на + * кадре FRAME_171_STAND_WITH_SWORD. После kid_init(SEQ_STAND) кадр + * обычный, стража в новой комнате нет, swordfight не запускается — и + * диспетчер каждый кадр не делает НИЧЕГО. Это болезнь ЧИТА: в оригинале + * телепорта между комнатами нет, попасть в режим боя без соперника + * нечем. Меч в инвентаре (pop_have_sword) НЕ теряем — только состояние + * схватки. */ + Kid.sword = SWORD_0_SHEATHED; + holding_sword = 0; + offguard = 0; + guard_refrac = 0; + return 1; +} + +/* ---- ОТЛАДКА: номер отрисованной комнаты в верхнем борте ------------- */ + +static uint8_t room_label_sig = 0xFF; /* последняя показанная комната */ +static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */ + +void pop_dbg_room_label(uint8_t room) __banked +{ + uint8_t prev = room_label_sig, i; + room_label_sig = room; /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */ + if (room != prev) room_label_redraw = 2; + if (!room_label_redraw) return; + /* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный знакогенератор + * (_gfx_font_buf, 2 КБ статики в W2) и сажает кучу. Слева десятки, + * справа единицы: || |||| = 24. */ + setfillstyle(SOLID_FILL, BLACK); + bar(100, 0, 175, 5); + /* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная + * (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */ + setfillstyle(SOLID_FILL, 0x57); + for (i = 0; i < (uint8_t)(room / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5); + for (i = 0; i < (uint8_t)(room % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5); + room_label_redraw--; +} diff --git a/applications/PoP/roomtest/roomtest_cold.h b/applications/PoP/roomtest/roomtest_cold.h new file mode 100644 index 0000000..b175993 --- /dev/null +++ b/applications/PoP/roomtest/roomtest_cold.h @@ -0,0 +1,53 @@ +/* + * roomtest_cold.h — контракт между главным циклом (резидент W1/W2) и его + * ХОЛОДНОЙ половиной, вынесенной в банк (roomtest_cold.c). + * + * Зачем. В режиме huge под кучу остаётся то, что не занял _CODE+_DATA, и к + * уровню 8 запас упал ниже килобайта. Главный цикл при этом наполовину + * состоит из кода, который отрабатывает РАЗ НА УРОВЕНЬ (старт/рестарт) или + * вообще только по отладочной клавише (обход комнат, лейбл номера) — держать + * его в дефицитном окне незачем. Кадровый путь (tick/draw/флип) остался в + * резиденте целиком: цена выноса — один трамплин на вызов, а он тут ноль раз + * за обычный кадр. + * + * Что ОСТАЛОСЬ в roomtest.c и почему: enter_room_side (её зовут и холодная + * половина, и переходы между комнатами в кадре падения) и рабочие массивы + * комнаты — они же и есть то состояние, ради которого функция вообще + * существует; банковый модуль читает их как обычные глобалы (данные банков + * линкуются в общий _DATA, --bank-data мы не включаем). + */ +#ifndef ROOMTEST_COLD_H +#define ROOMTEST_COLD_H + +#include + +/* ---- то, что холодная половина берёт у главного цикла ---------------- */ + +extern uint8_t room_fg[30]; /* тайлы ОТРИСОВАННОЙ комнаты (fg & 0x1F) */ +extern uint8_t cur_room; /* её номер (drawn_room) */ +extern uint8_t pop_checkpoint; /* checkpoint (seg000:270) — ставит уход */ + /* влево из комнаты 7 уровня 3 */ + +/* Вход в комнату БЕЗ перехода за Кидом (старт уровня, респавн, чит-обход): + * страж такой смены не «видит» и остаётся в своей комнате. */ +void enter_room(uint8_t room); + +/* ---- холодная половина ----------------------------------------------- */ + +/* start_level (seg003:0055) + do_startpos + set_start_pos: поставить Кида в + * стартовую позицию уровня. Один вход и для запуска, и для рестарта после + * смерти / выпадения из уровня — в оригинале это буквально один путь. */ +void pop_start_level(void) __banked; + +/* ОТЛАДОЧНЫЙ обход комнат по номеру ('+' / '−'). dir: 1 — следующая, + * 2 — предыдущая. Ставит Кида на подходящий пол новой комнаты и выводит + * его из боя. Возвращает 1, если комната сменилась (вызывающий обязан + * вернуть себе draw-страницу и обнулить счётчик кадров смерти). */ +uint8_t pop_dbg_roomnav(uint8_t dir) __banked; + +/* ОТЛАДОЧНЫЙ лейбл номера комнаты палочками в верхнем борте. Рисует ТОЛЬКО + * при смене комнаты (и по разу на каждую страницу дабл-буфера), поэтому + * звать можно каждый кадр. */ +void pop_dbg_room_label(uint8_t room) __banked; + +#endif /* ROOMTEST_COLD_H */ diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index 11c4728..a6b968b 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -39,6 +39,11 @@ OBJS_grab := $(OBJS_phys) OBJS_wall := $(OBJS_phys) # t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень). OBJS_char := $(OBJS_phys) +# t_mouse — спецсобытие уровня 8 (мышь). ЕДИНСТВЕННЫЙ набор, куда линкуется +# guards.c: здесь проверяется сам ИИ (autocontrol_mouse) вместе с рождением +# (pop_check_mouse) и общей физикой соперника — то есть ровно то, что в +# остальных наборах заменено ручным прогоном play_guard_frame. +OBJS_mouse := $(OBJS_phys) build/eng_guards.rel include $(TESTKIT)/host-tests.mk diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index baca337..16da24f 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -232,6 +232,14 @@ uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos) return tk_room_fg[room][tilepos]; } +/* pop_level_tile (то же самое, но с маппингом окна W0 внутри) — её зовёт + * спецсобытие подъёма скелета из guards.c; тому единственному набору, где + * guards.c линкуется (t_mouse), достаточно чтения из tk_room_fg. */ +uint8_t pop_level_tile(uint8_t room, uint8_t tilepos) +{ + return pop_level_tile_raw(room, tilepos); +} + /* Запись в «данные уровня» — у теста это тот же tk_room_fg. Нужна с тех * пор, как do_pickup правит страницу уровня сразу (BUG-POTION-STRIPE). */ void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile) diff --git a/applications/PoP/roomtest/tests-host/t_mouse.c b/applications/PoP/roomtest/tests-host/t_mouse.c new file mode 100644 index 0000000..f3deb63 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_mouse.c @@ -0,0 +1,178 @@ +/* + * t_mouse — спецсобытие уровня 8: МЫШЬ (do_timers seg003:0545, + * do_mouse seg003:0ABA, autocontrol_mouse seg002:07EB). + * + * Единственный набор, куда линкуется guards.c: проверяется не физика (её + * держат t_phys/t_char), а связка «условие запуска → рождение → ИИ → + * нажатие кнопки → уход». Всё это ветки по charid внутри общего кода + * соперника, и сломать их можно, ничего не трогая в самой мыши. + * + * Кадр соперника здесь полный, с ИИ — pop_guard_tick() + pop_guard_phys_tick(), + * как в главном цикле; sc_guard_run для этого не годится (он намеренно без ИИ). + */ +#include + +#include "tcheck.h" +#include "pop_kid.h" +#include "pop_map.h" +#include "pop_guard.h" +#include "pop_geom.h" +#include "pop_state.h" +#include "pop_level.h" /* pop_current_level (под тестом — из stubs.c) */ +#include "pop_tune.h" +#include "scene.h" +#include "stubs.h" + +#define E 0 /* empty */ +#define F 1 /* floor */ +#define O 15 /* opener */ +#define W 20 /* wall */ + +/* Верхний ряд комнаты 16 в упрощённом виде: сплошной пол и кнопка на + * колонке 7 — там же, где она стоит в данных уровня 8. Провал колонок + * 4..6 намеренно засыпан: он мешает Киду, а не мыши, и в тест на ИИ + * притащил бы падение соперника. */ +static const uint8_t room_mouse[30] = { + F, F, F, F, F, F, F, O, F, F, + W, W, W, W, W, W, W, W, W, W, + W, W, W, W, W, W, W, W, W, W, +}; + +/* Поставить сцену уровня 8: Кид в комнате 16, дверь уровня открыта. */ +static void scene_level8(void) +{ + sc_room(room_mouse, POP_MOUSE_ROOM); + sc_kid_at(1, 0, 1); + Kid.room = POP_MOUSE_ROOM; + pop_guard_reset(); + pop_current_level = POP_MOUSE_LEVEL; + pop_leveldoor_open = 1; + tk_log_clear(); +} + +/* Один полный кадр соперника, как в главном цикле roomtest.c. */ +static void mouse_frame(void) +{ + pop_guard_tick(); + pop_guard_phys_tick(); +} + +/* ---- рождение -------------------------------------------------------- */ + +TC_TEST(mouse_appears_after_delay) +{ + uint16_t i; + scene_level8(); + + /* Счётчик стартует с 1 («дверь открыта») и растёт по кадру, срабатывая + * на значении POP_MOUSE_DELAY — то есть кадров до мыши на два меньше. + * Прогоняем всё, кроме последнего: слот обязан быть ещё пуст. */ + for (i = 0; i < POP_MOUSE_DELAY - 2; i++) pop_check_mouse(); + TC_EQ(Guard.charid, 0); + + pop_check_mouse(); /* тот самый кадр */ + TC_EQ(Guard.charid, CHARID_24_MOUSE); + TC_EQ(Guard.x, POP_MOUSE_START_X); + TC_EQ(Guard.curr_row, 0); + TC_EQ(Guard.direction, -1); /* dir_FF_left */ + TC_EQ(guardhp_curr, 1); + TC_EQ(Guard.room, POP_MOUSE_ROOM); /* унаследована от Кида (loadkid) */ +} + +/* Счётчик тикает ТОЛЬКО пока Кид в комнате мыши: в оригинале это то же + * условие, что и запуск (Char.room == mouse_room), поэтому 12,5 секунды + * отсчитываются с возвращения игрока, а не с открытия двери. */ +TC_TEST(mouse_timer_frozen_outside_room) +{ + uint16_t i; + scene_level8(); + Kid.room = POP_MOUSE_ROOM + 1; + for (i = 0; i < POP_MOUSE_DELAY * 2; i++) pop_check_mouse(); + TC_EQ(Guard.charid, 0); + TC_EQ(pop_leveldoor_open, 1); /* не сдвинулся ни на кадр */ +} + +/* Закрытая дверь уровня — мыши нет вовсе (она подсказка на обратном пути). */ +TC_TEST(mouse_needs_open_leveldoor) +{ + uint16_t i; + scene_level8(); + pop_leveldoor_open = 0; + for (i = 0; i < POP_MOUSE_DELAY * 2; i++) pop_check_mouse(); + TC_EQ(Guard.charid, 0); +} + +/* ---- ИИ: пробежала, нажала, ушла ------------------------------------- */ + +/* Родить мышь, не гоняя POP_MOUSE_DELAY пустых кадров. */ +static void spawn_mouse(void) +{ + scene_level8(); + pop_leveldoor_open = POP_MOUSE_DELAY - 1; + pop_check_mouse(); +} + +TC_TEST(mouse_runs_left_and_presses_button) +{ + uint8_t i; + spawn_mouse(); + TC_EQ(Guard.charid, CHARID_24_MOUSE); + + /* Бежит влево: до порога разворота ей ~35 пикселей по 3-5 за кадр. */ + for (i = 0; i < 24; i++) mouse_frame(); + TC_TRUE(Guard.x < POP_MOUSE_START_X); + + /* По дороге наступила на кнопку (0,7) — общий check_press физики + * соперника, тот же, которым кнопки жмут страж и тень. */ + TC_TRUE(tk_log_count(TK_BUTTON) >= 1); +} + +TC_TEST(mouse_turns_back_and_disappears) +{ + uint8_t i; + spawn_mouse(); + + /* 120 кадров с запасом покрывают путь туда (200→166), стойку seq_107 + * (11 кадров) и путь обратно (166→200). */ + for (i = 0; i < 120; i++) { + mouse_frame(); + if (Guard.direction == DIR_56_NONE) break; + } + TC_EQ(Guard.direction, DIR_56_NONE); /* clear_char: ушла со сцены */ + TC_EQ(guardhp_curr, 0); +} + +/* ---- ветки по charid у соседнего кода -------------------------------- */ + +/* Мышь Кида «не видит»: иначе он выхватит на неё меч (seg003:0965). */ +TC_TEST(mouse_is_not_seen_by_kid) +{ + spawn_mouse(); + Guard.curr_row = Kid.curr_row; /* один ряд — условие луча */ + pop_check_can_guard_see_kid(); + TC_EQ(can_guard_see_kid, 0); +} + +/* leave_guard мышь не записывает в данные уровня (seg002:02F5): вернувшись + * в комнату, игрок не должен встретить «стража» с её параметрами. + * Комнату берём из диапазона таблицы заглушки (TK_ROOMS = 8). */ +TC_TEST(mouse_is_not_saved_as_room_guard) +{ + spawn_mouse(); + Guard.room = 1; + tk_gs_tile[0] = 0xEE; /* метка «никто сюда не писал» */ + pop_guard_leave(); + TC_EQ(tk_gs_tile[0], 0xEE); +} + +void main(void) +{ + sc_init(); + TC_RUN(mouse_appears_after_delay); + TC_RUN(mouse_timer_frozen_outside_room); + TC_RUN(mouse_needs_open_leveldoor); + TC_RUN(mouse_runs_left_and_presses_button); + TC_RUN(mouse_turns_back_and_disappears); + TC_RUN(mouse_is_not_seen_by_kid); + TC_RUN(mouse_is_not_saved_as_room_guard); +}