Мышь уровня 8 + холодная половина главного цикла в банк 8

Спецсобытие уровня 8 (SDLPoP seg003:0545/0ABA, seg002:07EB): когда дверь
уровня открыта, а Кид остаётся в комнате 16, через 150 кадров приходит мышь,
пробегает справа налево по верхнему ряду, наступает на кнопку (0,7) —
решётка (0,3) открывается — и убегает.

Ассетов не потребовалось: кадры 186..188 идут по таблице Кида, их спрайты
(images 130..132) уже лежат в kid16.atl, отрисовка подхватывает мышь сама.

- guards.c: pop_check_mouse, autocontrol_mouse, ветки мыши в
  autocontrol_opponent / play_guard / check_can_guard_see_kid;
- pop_leveldoor_open стал word, как в оригинале (в него же считается
  задержка — байт переполнился бы через 255 кадров);
- leave_guard больше не записывает тень и мышь в данные уровня (seg002:02F5);
- полосы HP у мыши нет (seg000:1159);
- tests-host/t_mouse: 17 проверок, единственный набор с guards.c.

Разгрузка резидента (куча в huge упала до 879 Б): pop_start_level,
find_start_level_door, чит-навигация по комнатам и лейбл номера уехали в
roomtest_cold.c (--bank 8, n_banks = 8).  _CODE 25653 -> 24825, куча -> 1707 Б;
кадровый путь не тронут.

Проверено: tests-host 6/6, make size-check OK, живьём в MAME (мышь появилась,
нажала кнопку, решётка поехала вверх, мышь ушла).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 15:21:20 +03:00
parent af04a8141e
commit 361c9d9f78
17 changed files with 803 additions and 250 deletions
+2 -2
View File
@@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
ALLOCS ?= 3000 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) — ВКЛЮЧЕНО по # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # умолчанию, пока идёт работа с производительностью; 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 kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
cd $(TC) && python3 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)
+35 -30
View File
@@ -1,4 +1,4 @@
# Точка входа для следующей сессии (обновлено 2026-08-12) # Точка входа для следующей сессии (обновлено 2026-08-12, вечер)
Файл для старта с чистого контекста: где всё стоит, что делать первым, какие Файл для старта с чистого контекста: где всё стоит, что делать первым, какие
грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам: грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам:
@@ -6,39 +6,37 @@
--- ---
## 1. СЛЕДУЮЩАЯ ЗАДАЧА: живая проверка зелий в MAME ## 1. СЛЕДУЮЩАЯ ЗАДАЧА: smoke-прогон уровня 8
Код зелья медленного падения НАПИСАН (все шесть шагов плана Зелья приняты пользователем (уровень 2 — «−HP», уровень 7 — перо), уровень 7
[`TASKS_OPEN.md#l7-feather`](TASKS_OPEN.md#l7-feather)) и закрыт хост-тестом, считается прошедшим smoke. Спецсобытие уровня 8 —
но в MAME ещё не смотрелся. Проверить две сцены: [L8-MOUSE](TASKS_CLOSED.md#l8-mouse) — **закрыто 2026-08-12**: мышь
портирована, покрыта хост-набором `t_mouse` (17 проверок) и проверена живьём в
MAME (появилась, нажала кнопку (0,7) комнаты 16, решётка (0,3) поехала вверх,
ушла и погасла). Новых тайлов уровень 8 не приносит вовсе.
- **уровень 7, комната 1** — зелье пера: зелёные пузырьки, зелёная вспышка Осталось пройти сам уровень: он длинный, с возвратом через два чомпера
при питье, спуск в шахту заметно медленнее обычного и без урона; комнаты 4, и стражи там сильнее прежних — комната 5 держит **skill 7**
- **уровень 2, комната 13, тайл (1,3)** — зелье «−HP»: пузырьки СИНИЕ (были (максимум из встречавшихся), комнаты 12/22/24 — skill 2/3/3.
красные — цвет по типу зелья не был портирован вовсе), HP убавляется на
единицу, экран краснеет ровно один раз.
Заодно попадают под проверку `pop_flash_color` (жёлтый/красный/зелёный) и Правило появления мыши (сверено с оригиналом, детали в
атлас `pop_pot` (23 → 37 спрайтов: добавлены зелёный и синий наборы кадров [TASKS_CLOSED](TASKS_CLOSED.md#l8-mouse)): счётчик — сам `leveldoor_open`,
пузырька). растёт только пока Кид в комнате 16, срабатывание ровно на 150, **второго
раза нет**; сбрасывает только рестарт уровня. Тупика это не создаёт — низом
через провал есть путь к двери уровня ценой 1 HP.
**Зацеп в падении — закрыт 2026-08-12** Резидент разгружен ([MEM-COLD1](TASKS_CLOSED.md#mem-cold1)): холодная половина
([GRAB-BELOW-ROOM](BUGS_CLOSED.md#grab-below-room)). Оказалось, что зацеп главного цикла уехала в **банк 8** (`roomtest_cold.c`), куча 879 → **1707 Б**.
из падения был жив, а сломан был спуск через НИЖНЮЮ кромку комнаты (уровень 7, Следующие кандидаты — [MEM-COLD2](TASKS_OPEN.md#mem-cold2).
комната 14): `do_fall` не пускал ряд персонажа до 3, и `check_grab` на этом
отрезке не звался ни разу. Плюс восстановлена ветка `ACT_MIDAIR` (кадры
102..105 — начало падения). Проверено хост-тестом `t_grab` и живьём в MAME.
Осталась ощущенческая часть — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing): Ощущенческий хвост прежний — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing),
зацеп при входе на уровень 7 срабатывает менее стабильно, чем в оригинале разбирается после готовности всех уровней (решение пользователя).
(подозрение на клавиатурный модуль и на общий темп игры, `L1-SPEED`).
**Решение пользователя: разбирать после того, как будут готовы все уровни.**
## 2. Состояние репозитория ## 2. Состояние репозитория
Всё закоммичено, `main` чист. `tests-host`: 5 наборов проходят Всё закоммичено, `main` чист. `tests-host`: **6 наборов** проходят
(`[char] 65`, `[geom] 3144`, `[grab] 55`, `[phys] 1727`, `[wall] 1`). (`[char] 65`, `[geom] 3144`, `[grab] 55`, `[mouse] 17`, `[phys] 1727`,
`make size-check`: OK (65 программ, роста нет). `[wall] 1`). `make size-check`: OK (65 программ, роста нет).
Коммиты 2026-08-12: Коммиты 2026-08-12:
@@ -55,10 +53,17 @@
## 3. Состояние окружения ## 3. Состояние окружения
- **MAME запущена с мостом**, на HDD — roomtest, собранный с - **MAME запущена с мостом**, на HDD — roomtest, собранный с
`-DFIRST_LEVEL=7`. Пересобрать образ: `make hdd` (или `-DFIRST_LEVEL=8`. **ПРАВИЛО ПОЛЬЗОВАТЕЛЯ: перед сборкой образа —
`make PROF_FLAGS="-DPROF_BORDER=1 -DFIRST_LEVEL=N" hdd` — при смене `make clean`, потом `make`, потом `make hdd`** (memory
`FIRST_LEVEL` нужен `touch roomtest.c`, иначе не переедет), после чего `mame_build_clean_rule`). Без этого `make hdd` кладёт на диск ПРОШЛУЮ
MAME **обязана** пройти полный рестарт (memory `mame_hdd_rebuild_restart`). сборку: `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` - **Остановка MAME — `mame/v306/bridge_cmd.sh 'cmd exit'`**, НЕ `kill`
(правило пользователя). `pkill -f` бывает не срабатывает, и остаются ДВА (правило пользователя). `pkill -f` бывает не срабатывает, и остаются ДВА
инстанса, оба отвечающих на файловый IPC — тогда скриншоты приходят от инстанса, оба отвечающих на файловый IPC — тогда скриншоты приходят от
+97
View File
@@ -11,6 +11,103 @@
--- ---
## Уровни: спецсобытия
<a id="l8-mouse"></a>
### 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`).
<a id="mem-cold1"></a>
### 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).
---
## Приёмка уровней ## Приёмка уровней
<a id="pass-policy"></a> <a id="pass-policy"></a>
+32 -1
View File
@@ -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).
<a id="mem-cold2"></a>
### 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 — Уровни 1-4 играются (smoke). Дальше идём по порядку уровней; уровень 5 —
следующий. следующий.
+75
View File
@@ -98,6 +98,10 @@ void pop_check_can_guard_see_kid(void) __banked
int16_t left_pos, right_pos; int16_t left_pos, right_pos;
uint8_t tile; uint8_t tile;
/* МЫШЬ Кида «не видит» (seg003:0965): иначе он выхватит меч на зверька,
* пробегающего мимо. */
if (Guard.charid == CHARID_24_MOUSE) { can_guard_see_kid = 0; return; }
/* Первый множитель — порт seg003:702. ТЕНЬ боевая только на 12-м /* Первый множитель — порт seg003:702. ТЕНЬ боевая только на 12-м
* уровне; на 4/5/6 она Кида «не видит», и это не косметика: увидев её, * уровне; на 4/5/6 она Кида «не видит», и это не косметика: увидев её,
* Кид достаёт меч (control_standing, seg005:352), а сама тень идёт той * Кид достаёт меч (control_standing, seg005:352), а сама тень идёт той
@@ -198,6 +202,50 @@ void pop_check_skel(void) __banked
is_guard_notice = guard_refrac = 0; 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) -- * /* ---- Тень: спецсобытие входа в комнату (check_shadow, seg002:0064) -- *
* *
* На «шадоу»-уровнях слот соперника занимает ТЕНЬ, и занимает его ВМЕСТО * На «шадоу»-уровнях слот соперника занимает ТЕНЬ, и занимает его ВМЕСТО
@@ -638,6 +686,26 @@ static void autocontrol_shadow_level6(void)
move_1_forward(); 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 появится со своим /* autocontrol_shadow (seg002:081D) — по уровням. Ветка 12 появится со своим
* уровнем. */ * уровнем. */
static void autocontrol_shadow(void) static void autocontrol_shadow(void)
@@ -658,6 +726,8 @@ static void autocontrol_opponent(void)
/* autocontrol_skeleton (seg002:685): у скелета меч вынут ВСЕГДА — он не /* autocontrol_skeleton (seg002:685): у скелета меч вынут ВСЕГДА — он не
* бывает «неактивным», поэтому ветка inactive ему недоступна. */ * бывает «неактивным», поэтому ветка inactive ему недоступна. */
if (Char.charid == CHARID_4_SKELETON) Char.sword = SWORD_2_DRAWN; 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): к стражьему ИИ она не /* ТЕНЬ идёт своей веткой ЦЕЛИКОМ (seg002:0647): к стражьему ИИ она не
* сводится — на уровне 4 не дерётся вовсе. */ * сводится — на уровне 4 не дерётся вовсе. */
if (Char.charid == CHARID_1_SHADOW) { autocontrol_shadow(); return; } 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) 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) { if (Char.alive < 0 && guardhp_curr == 0) {
Char.alive = 0; Char.alive = 0;
/* on_guard_killed (seg006:1927) — у нас пока только звук победы, /* on_guard_killed (seg006:1927) — у нас пока только звук победы,
+4 -2
View File
@@ -807,8 +807,10 @@ void pop_hp_draw(void) __banked
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++) 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); 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); const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
uint16_t w, h; uint16_t w, h;
gfx_w0_map(gp[0].page); gfx_w0_map(gp[0].page);
+4
View File
@@ -185,6 +185,10 @@ void pop_guard_load_frame(void)
void pop_guard_leave(void) void pop_guard_leave(void)
{ {
if (Guard.charid == 0) return; /* стража не было */ 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, pop_guard_state_save(Guard.room,
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */ (uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
Guard.direction, Guard.x, guard_skill, Guard.direction, Guard.x, guard_skill,
+12
View File
@@ -68,6 +68,12 @@
#define CHARID_2_GUARD 2 #define CHARID_2_GUARD 2
#define CHARID_4_SKELETON 4 #define CHARID_4_SKELETON 4
#define CHARID_5_PRINCESS 5 #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 убирает /* dir_56_none (types.h:402): «направления нет» — им clear_char убирает
* персонажа со сцены. Значение 0x56 в int8_t = 86, то есть заведомо не * персонажа со сцены. Значение 0x56 в int8_t = 86, то есть заведомо не
@@ -191,6 +197,12 @@ void pop_check_can_guard_see_kid(void) __banked;
* в кадр, ДО логики персонажей (порядок play_frame, seg000:868). */ * в кадр, ДО логики персонажей (порядок play_frame, seg000:868). */
void pop_check_skel(void) __banked; 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) — СПЕЦСОБЫТИЕ входа в комнату: на «шадоу»- /* check_shadow (seg002:0064) — СПЕЦСОБЫТИЕ входа в комнату: на «шадоу»-
* уровнях слот соперника занимает ТЕНЬ, а не страж из данных. Звать ВМЕСТО * уровнях слот соперника занимает ТЕНЬ, а не страж из данных. Звать ВМЕСТО
* pop_guard_enter при входе в комнату; вернула 1 — событие обработано и * pop_guard_enter при входе в комнату; вернула 1 — событие обработано и
+7 -2
View File
@@ -27,8 +27,13 @@ uint8_t pop_droppedout;
/* leveldoor_open (seg007:456): дверь уровня доехала до верха. Ставит /* leveldoor_open (seg007:456): дверь уровня доехала до верха. Ставит
* анимация двери (pop_trob, банк 6), читает спецсобытие подъёма скелета * анимация двери (pop_trob, банк 6), читает спецсобытие подъёма скелета
* (guards.c, банк 1) — снова случай «данные через границу банков». * (guards.c, банк 1) — снова случай «данные через границу банков».
* Сбрасывается стартом уровня (seg003:97). */ * Сбрасывается стартом уровня (seg003:97).
uint8_t pop_leveldoor_open; *
* ШИРИНА — word, как в оригинале (data.h:361), а не byte: на уровне 8
* do_timers СЧИТАЕТ этой же переменной время до появления мыши
* (`++leveldoor_open` каждый кадр, пока Кид в комнате 16), и байт
* переполнился бы через 255 кадров, обнулив признак «дверь открыта». */
uint16_t pop_leveldoor_open;
/* ---- Шина управления (seg006 globals) ------------------------------ * /* ---- Шина управления (seg006 globals) ------------------------------ *
* Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка — * Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка —
+4 -2
View File
@@ -43,8 +43,10 @@ extern int pop_leveldoor_ybottom;
extern uint8_t pop_droppedout; extern uint8_t pop_droppedout;
/* leveldoor_open (seg007:456): 1 — дверь уровня открыта полностью. Пишет /* leveldoor_open (seg007:456): 1 — дверь уровня открыта полностью. Пишет
* анимация двери (банк 6), читает подъём скелета (банк 1). */ * анимация двери (банк 6), читает подъём скелета (банк 1). Word, как в
extern uint8_t pop_leveldoor_open; * оригинале: на уровне 8 в него же СЧИТАЕТСЯ задержка до мыши (см.
* pop_state.c и pop_check_mouse в guards.c). */
extern uint16_t pop_leveldoor_open;
/* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c. /* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c.
* Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на * Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на
+19
View File
@@ -90,6 +90,25 @@
* (TASKS_OPEN.md, L1-SPEED), а не свойство зелья. */ * (TASKS_OPEN.md, L1-SPEED), а не свойство зелья. */
#define POP_FEATHER_FRAMES 225 #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) ------------ * /* ---- Уровень зелий (SDLPoP: «potions level», seg006:1894) ------------ *
* На нём синие склянки «вреда» забирают ПОЛОВИНУ запаса HP, а не единицу. * На нём синие склянки «вреда» забирают ПОЛОВИНУ запаса HP, а не единицу.
* В оригинале это условие `current_level == 15` прямо в proc_get_object. */ * В оригинале это условие `current_level == 15` прямо в proc_get_object. */
+19 -211
View File
@@ -21,6 +21,7 @@
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */ #include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
#include "pop_cheat.h" #include "pop_cheat.h"
#include "pop_tune.h" #include "pop_tune.h"
#include "roomtest_cold.h" /* холодная половина цикла — в банке */
__sfr __at (0xFE) io_border; __sfr __at (0xFE) io_border;
@@ -54,7 +55,9 @@ __sfr __at (0xFE) io_border;
* оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw * оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw
* (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления), * (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления),
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина * 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
* фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты). * фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты),
* 8 = roomtest_cold (ХОЛОДНАЯ половина главного цикла: старт уровня и
* отладочная навигация — см. roomtest_cold.h).
* Отрисовка персонажей и холодная половина фона вынесены из банка 2 * Отрисовка персонажей и холодная половина фона вынесены из банка 2
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть * отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
* куда — тайлы поздних уровней, чомперы, зеркало. * куда — тайлы поздних уровней, чомперы, зеркало.
@@ -64,7 +67,7 @@ __sfr __at (0xFE) io_border;
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку * останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра * runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */ * доходила до конца загрузки ресурсов и вставала намертво в DSS. */
const uint8_t n_banks = 7; const uint8_t n_banks = 8;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */ /* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats; uint8_t pop_cheats;
@@ -119,13 +122,14 @@ static void kbd_idle(void)
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы /* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */ * чтобы 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), /* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и * [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
* смотрит наружу (колонка −2 / 11). См. pop_level.h. */ * смотрит наружу (колонка −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 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 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) */ static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid /* Рендер-смещение 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_rows; /* маска рядов, ждущих редроя (переживает 2 кадра) */
static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */ static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */
/* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая): /* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7
* sig = последняя показанная комната, redraw = остаток кадров (2 = обе страницы). */ * уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет
static uint8_t room_label_sig = 0xFF; * стартовую позицию. Сбрасывается началом игры и прохождением уровня —
static uint8_t room_label_redraw; * ровно как в оригинале (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 /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
* (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и * (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 ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */
#define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */ #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) int main(void)
{ {
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1 /* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
@@ -663,89 +565,13 @@ int main(void)
#ifdef ROOMNAV #ifdef ROOMNAV
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд, /* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна * '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
* комната. * комната. Сама навигация — в банке (roomtest_cold.c): она нужна
* * только по клавише, а место в резиденте дефицитно. */
* Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната
* смотрелась в исходном состоянии». Убран: reset обнуляет room_seen,
* и модификаторы перечитываются из уровня заново, то есть чит
* ОТМАТЫВАЕТ МИР — открытые/закрытые ворота, выдвинутые пики, нажатые
* кнопки. Пока ворота с bg=1 ошибочно стартовали закрытыми, откат был
* не виден; после порта load_alter_mod он выглядит как баг «ворота
* снова открылись» (уровень 1, решётка комнаты 5 (0,9), которую
* закрывает стартовый триггер do_startpos). Навигация обязана только
* телепортировать; исходное состояние даёт перезапуск уровня. */
{ {
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) | uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1)); ((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
if (nav && !nav_prev) { if (nav && !nav_prev && pop_dbg_roomnav((uint8_t)(nav & 1 ? 1 : 2)))
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();
dead_frames = 0; 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; nav_prev = nav;
} }
#endif #endif
@@ -755,6 +581,7 @@ int main(void)
gfx_set_draw_page(back); gfx_set_draw_page(back);
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы — /* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
@@ -1045,26 +872,7 @@ int main(void)
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера), * при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */ * а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
#ifdef ROOMNAV #ifdef ROOMNAV
{ pop_dbg_room_label(cur_room); /* номер комнаты палочками — в банке */
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--;
}
}
#endif #endif
gfx_wait_vsync(); gfx_wait_vsync();
+249
View File
@@ -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 <graphics.h>
#include <gfx.h>
#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--;
}
+53
View File
@@ -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 <stdint.h>
/* ---- то, что холодная половина берёт у главного цикла ---------------- */
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 */
@@ -39,6 +39,11 @@ OBJS_grab := $(OBJS_phys)
OBJS_wall := $(OBJS_phys) OBJS_wall := $(OBJS_phys)
# t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень). # t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень).
OBJS_char := $(OBJS_phys) 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 include $(TESTKIT)/host-tests.mk
@@ -232,6 +232,14 @@ uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
return tk_room_fg[room][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. Нужна с тех /* Запись в «данные уровня» — у теста это тот же tk_room_fg. Нужна с тех
* пор, как do_pickup правит страницу уровня сразу (BUG-POTION-STRIPE). */ * пор, как do_pickup правит страницу уровня сразу (BUG-POTION-STRIPE). */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile) void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
@@ -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 <stdint.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_state.h"
#include "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 кадров) и путь обратно (166200). */
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);
}