From 67d62e3e8b0cf8b3c312b6e5b5f5e7171a2842bc Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 13 Aug 2026 18:21:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=80=D0=BE=D0=B2=D0=BD=D0=B8=2012=20?= =?UTF-8?q?=D0=B8=2013:=20=D1=82=D0=B5=D0=BD=D1=8C,=20=D0=94=D0=B6=D0=B0?= =?UTF-8?q?=D1=84=D0=B0=D1=80,=20=D0=BF=D0=B0=D0=B4=D0=B0=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BB=D0=B8=D1=82=D1=8B=20+=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20=D1=81=D0=BB=D0=BE=D1=91=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Уровень 12 (тень) — порт seg002/seg006: - check_shadow: подъём тени в комнате 15 по условию «меч подобран», init_shad_12, вход падением (seq 7); - autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ; - общий урон (ранил тень — ранил себя) и check_killed_shadow (убил тень — убил себя); - таймер вспышки слияния 42 -> -1, sword_disappears при уходе из комнаты 18, появление плит в комнатах 2/13 после слияния. Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт сбросить HP. Чит навигации по комнатам этот триггер придерживает — иначе комнату 23 двенадцатого уровня не посмотреть в принципе. Уровень 13 (Джафар): guard_notice_timer (фора после встречи), on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три исключения loose-механики. Плюс спрайты визиря VIZIER.DAT — без них он рисовался обычным стражем; заодно цвет из данных комнаты применяется только к обычному стражу, как в оригинале. Падающие плиты — семь дефектов, найденных прогонами: - MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние терялись без щебня; - одиночные сигналы посадки/провала больше не затираются в кадре; - честный loose_fall: сбитая плита рождается в том же кадре, от места удара, с половинной скоростью; - при снятии плиты метится и сосед справа (висел передний торец); - heal и отрисовка кусков разнесены на два прохода; - куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y (compare_curr_objs для пары 0x80 сортирует наоборот); - wall_pattern убран из ceil_over_kid_tile: у оригинала узор из draw_tile_bottom идёт в фон, а не в передний слой. Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44). TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам. Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами тени при слиянии, чит навигации не запускает бесшовный переход, чит K убивает через штатный путь смерти. Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER, GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md). Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/README.md | 1 + applications/PoP/docs/impl_diff.md | 159 ++++++ applications/PoP/docs/levels_12_15_plan.md | 462 ++++++++++++++++++ applications/PoP/poc/res/vizier/g0.atl | Bin 0 -> 9711 bytes applications/PoP/poc/res/vizier/g1.atl | Bin 0 -> 10732 bytes applications/PoP/poc/res/vizier/g2.atl | Bin 0 -> 9865 bytes applications/PoP/poc/res/vizier/g3.atl | Bin 0 -> 3283 bytes applications/PoP/poc/res/vizier/g4.atl | Bin 0 -> 1194 bytes applications/PoP/roomtest/BUGS_OPEN.md | 351 ++++++++++++- applications/PoP/roomtest/Makefile | 14 +- applications/PoP/roomtest/TASKS_OPEN.md | 145 ++++++ applications/PoP/roomtest/guards.c | 232 ++++++++- applications/PoP/roomtest/pop_bg.c | 74 ++- applications/PoP/roomtest/pop_bg.h | 8 + applications/PoP/roomtest/pop_cdraw.c | 13 +- applications/PoP/roomtest/pop_guard.c | 43 +- applications/PoP/roomtest/pop_guard.h | 82 +++- applications/PoP/roomtest/pop_guard_cold.c | 11 +- applications/PoP/roomtest/pop_guard_pal.h | 5 + applications/PoP/roomtest/pop_map.c | 352 ++++++++++++- applications/PoP/roomtest/pop_map.h | 33 ++ applications/PoP/roomtest/pop_room.c | 230 ++++++++- applications/PoP/roomtest/pop_state.c | 21 + applications/PoP/roomtest/pop_state.h | 13 + applications/PoP/roomtest/pop_trob.c | 53 ++ applications/PoP/roomtest/pop_tune.h | 25 + applications/PoP/roomtest/roomtest.c | 56 +++ applications/PoP/roomtest/roomtest_cold.c | 29 +- applications/PoP/roomtest/tests-host/Makefile | 6 + applications/PoP/roomtest/tests-host/scene.c | 6 + applications/PoP/roomtest/tests-host/stubs.c | 6 +- applications/PoP/roomtest/tests-host/stubs.h | 8 +- applications/PoP/roomtest/tests-host/t_char.c | 63 +++ .../PoP/roomtest/tests-host/t_jaffar.c | 349 +++++++++++++ applications/PoP/roomtest/tests-host/t_phys.c | 36 +- .../PoP/roomtest/tests-host/t_shadow.c | 332 +++++++++++++ applications/PoP/toolchain/pop_pack_guard.py | 30 +- 37 files changed, 3169 insertions(+), 79 deletions(-) create mode 100644 applications/PoP/docs/levels_12_15_plan.md create mode 100644 applications/PoP/poc/res/vizier/g0.atl create mode 100644 applications/PoP/poc/res/vizier/g1.atl create mode 100644 applications/PoP/poc/res/vizier/g2.atl create mode 100644 applications/PoP/poc/res/vizier/g3.atl create mode 100644 applications/PoP/poc/res/vizier/g4.atl create mode 100644 applications/PoP/roomtest/tests-host/t_jaffar.c create mode 100644 applications/PoP/roomtest/tests-host/t_shadow.c diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index b886c6d..d864cdf 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -11,6 +11,7 @@ | [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему | | [`perf_backlog.md`](perf_backlog.md) | **Отложенная оптимизация отрисовки** с замерами + как мерить (wait-state'ы, границы кадра) | | [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | +| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 | | [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки | | [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди | | [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии из `BUGS_CLOSED.md`, дифф против SDLPoP | diff --git a/applications/PoP/docs/impl_diff.md b/applications/PoP/docs/impl_diff.md index 564015d..3cc81cb 100644 --- a/applications/PoP/docs/impl_diff.md +++ b/applications/PoP/docs/impl_diff.md @@ -245,3 +245,162 @@ offscreen неперевёрнутым, а зеркалит только при **Что проверять при регрессе.** Уровень 9: выпить зелёное зелье — картинка переворачивается ровно один раз, лишних языков пламени по комнате нет. Чит U даёт тот же результат (он идёт тем же путём). + +--- + +## Окклюзия воротами: спрашиваем про рисуемого персонажа, а не жёстко про Кида + +**Оригинал** (`draw_tile_fore`, seg008:0D15) первой строкой: + +```c +if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row && + Kid.curr_col == drawn_col - 1 && Kid.room != room_R) + draw_gate_fore(); +``` + +То есть бары ворот попадают в foretable — поверх всего нарисованного — когда +на тайле ворот стоит **именно Кид**. Это следствие устройства оригинала: +foretable ОДНА на весь проход тайлов, персонажи в неё уже добавлены, и +отдельного «переднего слоя на персонажа» там нет. + +**Мы** ради скорости не рисуем foretable целиком, а возвращаем куски тайлов +поверх ТОЛЬКО в прямоугольнике персонажа (`pop_fore_over_char`, см. memory +`pop_fore_layer_cost`: полный проход стоил 78 % кадра). Проход идёт по +персонажу, значит и вопрос естественно задавать про него — +`pop_gate_over_char(ch)`, а не про глобального `Kid`. + +**Чем платим.** Наш вариант — надмножество оригинального: страж (или тень), +стоящий в проёме ворот, у нас уходит ЗА решётку, а в оригинале остался бы +нарисованным поверх неё, пока на том же тайле нет Кида. Визуально это +правильнее, но формально расхождение. Обратной разницы нет: во всех случаях, +где оригинал рисует бары поверх, рисуем и мы. + +**Что проверять при регрессе.** Уровень 10, комната 7, тайл (2,6): Кид, +стоящий в проёме ворот, виден ЗА прутьями. Решение покрыто хост-тестами +(`tests-host/t_char.c`, набор `char_gate_*`) — отрисовка в харнесс не +линкуется, поэтому проверяется предикат. + +--- + +## Слияние с тенью: мигания Кида спрайтами тени нет + +**Файлы:** `roomtest/guards.c` (`autocontrol_shadow_level12`), +`roomtest/pop_cdraw.c`. +**Дата:** 2026-08-13. + +**Оригинал** (`draw_objtable_item`, seg008:20CA) во время вспышки слияния +(`united_with_shadow` считает 42 → 0) рисует КИДА как тень на чётных +значениях счётчика: тот же кадр уходит не обычным прозрачным блиттером, а +парой OR+XOR со сдвигом на пиксель. Получается мерцание «Кид/тень» +примерно полторы секунды. + +**Мы** рисуем всё это время обычного Кида, а само событие обозначаем белой +вспышкой фона (`pop_flash_color = POP_FLASH_WHITE`, 18 кадров) — она в +оригинале тоже есть и ставится тем же кодом. + +**Почему.** У нас Кид и соперник рисуются из РАЗНЫХ атласов своими +палитрами (`pop_cdraw.c`), а «тень» — это персонаж слота Guard с палитрой +комнаты; блиттеров OR/XOR в libbgi нет вовсе, прозрачность сделана +0xFF-подавлением записи. Воспроизвести эффект — значит завести Киду второй +набор спрайтов и второй путь блита ради 42 кадров за всю игру. + +**Чем платим.** Момент слияния читается только по вспышке и по тому, что +тень исчезла, — без «двоящегося» силуэта. На механику не влияет: счётчик +`pop_united_shadow` тикает и уходит в −1 независимо от отрисовки, а от него +зависят и повторный подъём тени, и появление плит в комнатах 2/13. + +**Что проверять при регрессе.** Уровень 12: после слияния экран белеет, +соперник пропал, HP-потолок вырос на единицу, тень в комнате 15 больше не +появляется. Логика покрыта `tests-host/t_shadow.c`. + +--- + +## Отложенный старт падающих плит (уровень 13): фаза 0 у нас «не анимируется» + +**Файлы:** `roomtest/pop_map.c` (`pop_check_fall_flo`, `pop_loose_tick`), +`roomtest/pop_trob.c` (`animate_loose`). +**Дата:** 2026-08-13. + +**Оригинал** (`check_fall_flo`, seg000:1317) раздаёт шести плитам ряда 2 +верхней комнаты модификатор `−(prandom(0xFF) & 0x0F)`, то есть 0..−15, и +заводит на каждую trob. Фаза считает вверх, проходит ноль и дальше идёт +обычным отсчётом до провала — плита падает через `n + 11` кадров. Ноль там +безопасен: плиту держит в игре СПИСОК trob, а не значение модификатора. + +**Мы** списка trob для loose текущей комнаты не держим — плита анимируется +ровно тогда, когда её фаза не ноль (`pop_loose_modif[pos] != 0`). Значит +счёт, дойдя до нуля, оборвался бы навсегда. Компенсируем двумя правками, +которые работают только в паре: + +* тик перескакивает ноль (`if (m == 0) m = 1`); +* стартовое значение берётся на единицу «отрицательнее» (`−n−1`). + +**Чем платим.** Ничем в наблюдаемом поведении: суммарная задержка остаётся +`n + 11` кадров, проверено арифметикой на обоих концах диапазона (n = 0 и +n = 15). Платим связностью — две правки в разных функциях, и убрать любую +одну нельзя. + +**Что проверять при регрессе.** Уровень 13, вход в комнату 23 (она же +стартовая): плиты сверху сыплются ВРАЗНОБОЙ, а не разом и не «никогда». +Логика покрыта `tests-host/t_jaffar.c` +(`jaffar_negative_phase_counts_through_to_fall` и парный контроль на другом +уровне). + +--- + +## Чит навигации по комнатам не запускает бесшовный переход уровня + +**Файлы:** `roomtest/roomtest_cold.c` (`pop_dbg_roomnav`), `roomtest/roomtest.c`, +`roomtest/pop_state.c` (`pop_nav_hold`). +**Дата:** 2026-08-13. + +**Оригинал** (`play_level_2`, seg000:0900) проверяет `Kid.room == 23` КАЖДЫЙ +кадр: уровень 12 кончается самим фактом присутствия Кида в комнате 23, двери +у него нет. Никакого «как он туда попал» там нет и быть не может — +телепорта между комнатами в игре 1989 года не существует. + +**Мы** держим этот триггер, пока Кид попал в комнату ЧИТОМ навигации +(`+`/`−`), и отпускаем на первой же смене комнаты обычным ходом. + +**Почему.** Чит перебирает комнаты ПО НОМЕРУ (1..24 с обёрткой), то есть +любой обход уровня 12 неизбежно наступает на 23-ю — и уровень молча +становится 13-м. Поймано на первом же прогоне 2026-08-13: проверяющий час +смотрел «комнату 20 уровня 12», которая на самом деле была комнатой 20 +уровня 13, и сравнивал её с картой не того уровня. Комнату 23 уровня 12 +читом не посмотреть в принципе. Это ровно та же болезнь чит-телепорта, что +BUG-CHEAT-FIGHT-1 (выход из боя), и лечится там же. + +**Чем платим.** Ничем в игре: в обычном прохождении Кид входит в комнату 23 +ногами, флаг снят, переход срабатывает как в оригинале. Расхождение видно +ТОЛЬКО при включённых читах. + +**Что проверять при регрессе.** Уровень 12: пройти в комнату 23 ногами — +уровень меняется на 13-й без заставки и без сброса HP. Обойти уровень +читом `+` через 23-ю — уровень НЕ меняется. + +--- + +## Чит «убить стража» (K) идёт через штатный путь смерти + +**Файлы:** `roomtest/pop_guard.c` (`pop_guard_kill`). +**Дата:** 2026-08-13. Решение пользователя. + +**Оригинал** (seg000:786) ставит `guardhp_delta = -guardhp_curr` И +`Guard.alive = 0`. А гейт события смерти в `play_guard` (seg006:1490) +требует `Char.alive < 0` — то есть у оригинала чит убивает стража В ОБХОД +`on_guard_killed`. На 13-м уровне это заметно: победа над Джафаром по читу +не ставит `leveldoor_open = 2`, и выход на 14-й не открывается. + +**Мы** `Guard.alive` в чите не трогаем: применённая дельта обнуляет HP, и +`play_guard` сам переводит стража в «умирает», вызвав `on_guard_killed` — +брызги, вспышка, флаг выхода. То есть чит даёт ровно «как будто убил Кид». + +**Почему.** Отладочный прогон 13-го уровня иначе требует каждый раз честно +выигрывать бой с Джафаром (skill 9, 6 HP) — это дорого по времени, а +проверять надо совсем другое. + +**Чем платим.** Ничем в игре: читы включаются флагом `pop_cheats`, в +релизной сборке они выключены. Расхождение наблюдаемо только с читами. + +**Что проверять при регрессе.** Уровень 13: `K` на Джафаре → белая вспышка, +уход ВЛЕВО открывает дверь уровня. Честная победа в бою даёт то же самое. diff --git a/applications/PoP/docs/levels_12_15_plan.md b/applications/PoP/docs/levels_12_15_plan.md new file mode 100644 index 0000000..533555b --- /dev/null +++ b/applications/PoP/docs/levels_12_15_plan.md @@ -0,0 +1,462 @@ +# Что осталось на уровнях 12, 13, 14, 15 и 0 + +Статус: разбор по `../SDLPoP/src/`, 2026-08-13. Продолжает +[`levels_plan.md`](levels_plan.md) (машинерия уровней и тайлсеты — уже +сделаны). Здесь только СПЕЦСОБЫТИЯ, которых у нас ещё нет. + +Правило проекта: источник истины — SDLPoP; все ссылки ниже даны на функцию и +строку, чтобы порт начинался с чтения, а не с гипотезы. + +--- + +## 0. Что из этой области УЖЕ есть + +Проверено грепом по `roomtest/`: + +| механика | где у нас | статус | +|---|---|---| +| слот соперника, общий на всех Char | `pop_guard.c`, `pop_cdraw.c` | готово | +| `check_shadow` (спецвход тени) | `guards.c:310` | готово для уровней 4/5/6 | +| `do_init_shad` + таблицы `init_shad_5/6` | `guards.c:284` | готово | +| ИИ тени 4/5/6 | `guards.c:599/658/680` | готово | +| диспетчер `autocontrol_shadow` | `guards.c:711` | ветки 12 НЕТ | +| боёвка (удар/блок/парирование/HP соперника) | `guards.c`, `pop_ctrl.c` | готово | +| `flash_color` / `flash_time` | `pop_map.c`, `roomtest.c` | готово | +| `add_life` | `pop_map.c` | готово | +| таблицы уровней (`guard_type`, `guard_hp`, `entry_pose`, `level_type`) | `pop_level_cold.c:41..52` | готово, включая 12=SHADOW, 13=VIZIER | +| loose-полы, `make_loose_fall`, mob | `pop_map.c`, `pop_room.c` | готово (без спецкейсов ур. 13) | + +То есть каркас есть весь; ниже — недостающие спецсобытия. + +--- + +## 1. Уровень 12 — тень: встреча, бой, слияние + +### 1.1 Подъём тени в комнате 15 (`check_shadow`, seg002:0070) + +```c +if (current_level == 12) { + if (!united_with_shadow && drawn_room == 15) { + Char.room = drawn_room; + if (get_tile(15, 1, 0) == tiles_22_sword) return; // меч ещё лежит + shadow_initialized = 0; + do_init_shad(init_shad_12, 7 /* fall */); + return; + } +} +``` + +Отличия от наших веток 5/6: тень поднимается **в падении** (seq 7), условием +служит содержимое тайла (меч уже подобран) и флаг `united_with_shadow`. +Таблица `init_shad_12 = {0x0F, 0x51, 0xE8, 0, 0, 0, 0, 0}` — то есть +x=81, y=232, вправо, колонка 0, ряд 0. + +**Что добавить:** ветку в `pop_check_shadow` + константу `init_shad_12` + +глобалы `united_with_shadow`, `shadow_initialized`. + +### 1.2 ИИ тени (`autocontrol_shadow_level12`, seg002:1184) + +Самая содержательная функция уровня. Три режима: + +1. **Первый кадр в комнате 15**: пока Кид не подошёл (`Opp.x < 150`) — + `shadow_initialized = 1`; иначе тень ещё раз падает (`do_init_shad`). +2. **Кид с мечом** (`Char.sword >= sword_2_drawn`) → тень дерётся обычным + `autocontrol_guard_active` (у нас есть, `guards.c:535`). Особый случай: + если тень уже ранена (`offguard != 0 && guard_refrac != 0`) — она убирает + меч (`move_4_down`). +3. **Кид убрал меч** → тень тоже убирает и идёт навстречу; на дистанции + `< 10` — **СЛИЯНИЕ**: + +```c +flash_color = color_15_brightwhite; flash_time = 18; +add_life(); // +1 к максимуму HP +united_with_shadow = 42; // время вспышки Кид-тень +Char.charid = charid_0_kid; savekid(); // Кид ПЕРЕЕЗЖАЕТ на место тени +clear_char(); // тень со сцены +``` + +Плюс «если Кид бежит к тени — тень бежит к Киду» (кадры бега 3..14 и +шага 127..132). + +**Что добавить:** `autocontrol_shadow_level12` в `guards.c` + ветку в +диспетчере `autocontrol_shadow` (там уже три ветки, будет четвёртая). + +### 1.3 Общий урон (`do_delta_hp`, seg000:1518) + +```c +if (Opp.charid == charid_1_shadow && current_level == 12 && guardhp_delta != 0) + hitp_delta = guardhp_delta; // ранил тень — ранил себя +``` + +Три строки, но без них бой с тенью теряет смысл. У нас `do_delta_hp` +портирован — добавить условие. + +### 1.4 Таймер вспышки (`do_timers`, seg003:503) + +```c +if (united_with_shadow > 0) { + --united_with_shadow; + if (united_with_shadow == 0) { --united_with_shadow; /* → -1 */ ... } +} +``` + +`united_with_shadow` живёт как счётчик, потом как «уже слились» (−1). +На него смотрят `check_shadow` и `check_can_guard_see_kid`. + +### 1.5 Луч видимости (`check_can_guard_see_kid`, seg003:702) + +```c +if ((Guard.charid != charid_1_shadow || current_level == 12) && ... +``` + +У нас (`guards.c:103`) условие про тень **нужно сверить**: на уровне 12 тень +ОБЯЗАНА быть видимой (иначе Кид не достанет меч и бой не начнётся). + +### 1.6 Меч исчезает (`sword_disappears`, seg002:0536) + +```c +if (current_level == 12 && Char.room == 18) { + get_tile(15, 1, 0); + curr_room_tiles[curr_tilepos] = tiles_1_floor; + curr_room_modif[curr_tilepos] = 0; +} +``` + +Срабатывает при уходе Кида ВПРАВО из комнаты 18 (`leave_room`, ветка 1). +У нас есть `pop_level_set_tile` — порт на пять строк. + +### 1.7 Переход 12 → 13: НЕТ двери уровня, есть «бесшовный выход» + +Проверено по данным (`res2012.bin` / `res2013.bin`) — портала действительно +нет, уровень кончается **фактом попадания в комнату**: + +```c +// play_level_2, seg000:900 +} else if (custom->tbl_seamless_exit[current_level] >= 0) { + if (Kid.room == /*23*/ custom->tbl_seamless_exit[current_level]) { + ++next_level; + stop_sounds(); + seamless = 1; + } +} +``` + +`tbl_seamless_exit[12] = 23` (пара «уровень, комната» читается из оригинального +`PRINCE.EXE`, options.c:724). Дальше геометрия складывается так: + +| | комната | ряд 1 | что происходит | +|---|---|---|---| +| ур. 12 | 13 | `floor bigpil empty empty wall wall …` | Кид бежит ВЛЕВО с колонки 0 | +| ур. 12 | 23 | `empty ×6, floor floor floor floor` | попал сюда → **уровень сменился** | +| ур. 13 | 23 | `floor bigpil floor ×6 bigpil floor` | старт: ряд 1, кол 9, `seq_84_run` | + +Связи: `комната 13.left = 23`, и в комнату 23 больше ниоткуда не войти. +Номер комнаты у обоих уровней **один и тот же (23)**, стартовая позиция +уровня 13 — `BP_START = 23, поз 19 (ряд 1, кол 9), dir 0`, а +`tbl_entry_pose[13] = 2` даёт «вбегающий» вход (`seq_84_run`, seg003:172). +То есть Кид вбегает в комнату слева-направо… нет, `Char.direction = +~level.start_dir` — влево, тем же ходом, каким выбежал из уровня 12. Швов не +видно. + +Что делает флаг `seamless` (всего два места, оба косметические): + +* `start_level`, seg003:158 — **НЕ сбрасывает HP**: Кид уносит на уровень 13 + то здоровье, с которым добежал; +* `show_level`, seg008:1861/1870 — не показывает заставку «LEVEL 13» и тут же + гасит флаг. + +Двери уровня при этом на карте есть, но не при делах: у уровня 12 она одна +(комната 3, tilepos 23) — это ВХОД (стартовая комната 12-го — 3), а у уровня +13 их две (комната 3 tilepos 13 и комната 5 tilepos 24) — это выходы, +открываемые кнопкой из `Jaffar_exit`. + +**Что это значит для нас.** Наш переход уровня сейчас идёт только через +`SEQ_END_LEVEL` в двери. Для 12-го нужен второй триггер — проверка в главном +цикле «`pop_current_level == 12 && cur_room == 23` → `pop_next_level++`» плюс +флаг `seamless`, который пропустит сброс HP в `pop_start_level`. Обе правки +маленькие и локальные. + +--- + +## 2. Уровень 13 — Джафар + +### 2.1 Кто такой Джафар + +`tbl_guard_type[13] = 3` → `VIZIER.DAT` (у нас в `pop_level_cold.c` уже 3). +`tbl_guard_hp[13] = 6`. Отдельного ИИ у него нет: + +```c +void autocontrol_Jaffar() { autocontrol_guard(); } // seg002:0697 +``` + +То есть **бой с Джафаром — обычный бой стражи**, отличаются только спрайты, +HP и три спецсобытия ниже. Это хорошая новость: боёвка у нас есть. + +### 2.2 Встреча (`meet_Jaffar`, seg002:0544) + +```c +if (current_level == 13 && leveldoor_open == 0 && Char.room == 3) { + play_sound(sound_29_meet_Jaffar); + guard_notice_timer = 28; // Джафар ждёт 28/12 ≈ 2.33 с +} +``` + +Срабатывает при уходе Кида ВПРАВО (`leave_room`, ветка 1). +Пара к нему — в `autocontrol_guard_inactive` (seg002:0734), она у нас уже +портирована (`guards.c:401`), но **без условия по уровню**: + +```c +if (can_guard_see_kid) { + if (current_level != 13 || guard_notice_timer == 0) move_down_forw(); +} +``` + +**Что добавить:** глобал `guard_notice_timer` + его тик в `do_timers` +(seg003:509) + оба условия. + +### 2.3 Победа (`on_guard_killed`, seg006:1929) + +```c +} else if (current_level == 13) { + flash_color = color_15_brightwhite; flash_time = 18; + is_show_time = 1; + leveldoor_open = 2; // ← ключ к выходу + play_sound(sound_43_victory_Jaffar); +} +``` + +и парная `Jaffar_exit` (seg002:0517), срабатывающая при уходе Кида ВЛЕВО: + +```c +if (leveldoor_open == 2) { get_tile(24, 0, 0); trigger_button(0, 0, -1); } +``` + +То есть смерть Джафара не открывает дверь сама — она ставит флаг, а дверь +открывается кнопкой, «нажатой» при уходе влево. `trigger_button` у нас есть. + +### 2.4 Падающие плиты (`check_fall_flo`, seg000:1319) + +```c +if (current_level == 13 && (drawn_room == 23 || drawn_room == 16)) { + curr_room = room_A; // комната СВЕРХУ + for (curr_tilepos = 22; curr_tilepos <= 27; ++curr_tilepos) + make_loose_fall(-(prandom(0xFF) & 0x0F)); // ОТРИЦАТЕЛЬНЫЙ модификатор +} +``` + +Вот это и есть «плиты появляются»: при входе в комнаты 23/16 шесть плит ряда 2 +комнаты СВЕРХУ получают отрицательную фазу — то есть отложенный старт, и +сыплются на Кида вразнобой. Зовётся из `check_the_end` при смене комнаты. + +**Три спецкейса уровня 13 в loose-механике**, без них это не работает: + +| место | что | зачем | +|---|---|---| +| `animate_loose`, seg007:823 | при `modif & 0x80` НЕ останавливать тряску | иначе отрицательная фаза не досчитает до нуля и плита не упадёт | +| `loose_make_shake`, seg007:949 | на уровне 13 сотрясение НЕ трясёт плиты | иначе отложенные плиты сбрасываются в 0x80 | +| `fell_on_your_head`, seg007:1218 | плита бьёт и в БЕГЕ (кадры 5..14) | на прочих уровнях бегущего не задевает | + +У нас первый пункт критичен: `pop_loose_tick` трактует бит 7 как «тряска» и +на `>= 0x84` гасит фазу — отрицательный старт умрёт, не начавшись. Заодно +это ровно та же ветка, что мы правили сегодня в +[`LOOSE-ROOM-CHANGE`](../roomtest/BUGS_OPEN.md#loose-room-change), так что +код на виду. + +### 2.5 Поза входа + +`tbl_entry_pose[13] = 2` — у нас в таблице уже есть; проверить, что режим 2 +(`seg003:172`) реализован. + +--- + +## 3. Уровень 14 — принцесса и конец игры + +Боя нет вовсе (`tbl_guard_type[14] = -1`). Всё сводится к одному событию: + +```c +// check_the_end, seg000:1299 +if (current_level == 14 && drawn_room == 5) end_sequence(); +``` + +`end_sequence` (seg001:573) → `end_sequence_anim` (seg001:332): катсцена +«Кид добежал до принцессы» — обнимаются, появляется мышь, затухание, Hall of +Fame. Персонажи там играются ТЕМ ЖЕ интерпретатором `seqtbl` +(`seq_108_princess_turn_and_hug`, `seq_101_mouse_stands_up`), то есть движок +у нас уже подходит — нужны спрайты принцессы (`PRINCESS.DAT`) и раскадровка. + +**Оценка:** это не игровая механика, а ролик. Логично делать вместе с интро +и межуровневыми вставками — отдельным банком, как и договаривались +(см. memory `pop_banking_architecture`). На проходимость игры не влияет: +достаточно довести Кида до комнаты 5 и показать заглушку. + +--- + +## 4. Уровень 15 — «уровень зелий» (защита от копирования) + +Не часть сюжета. Это экран проверки подлинности из оригинала: после +уровня `copyprot_level` игра подменяет номер на 15, показывает комнату с +**14 зельями, на которых нарисованы буквы**, и требует выпить нужное. + +Механика (всё под `USE_COPYPROT` в SDLPoP): + +| место | что делает | +|---|---| +| `play_level`, seg003:53 | `level_number == copyprot_level` → грузим 15 | +| `redraw_screen`, seg003:273 | поверх зелий рисуются БУКВЫ (`copyprot_letter`) | +| `load_alter_mod`, seg008:1204 | одно зелье в комнате делается «открытым» (тип 6) | +| `animate_potion`, seg007:259 | на уровне 15 своя ветка перерисовки | +| `up_pressed`/`do_pickup`, seg005:657 | выпитое зелье убирает букву из таблицы | +| `seq` эффект зелья, seg006:1896 | синие зелья на уровне 15 отнимают ПОЛОВИНУ HP | +| выход, seg000:700 | из 15 возвращаемся в `copyprot_level` | + +**Рекомендация: не портировать.** Это анти-пиратский экран 1989 года, +требующий книжки-манускрипта; SDLPoP держит его выключенным по умолчанию +(`enable_copyprot`). Единственное, что стоит взять — **половинный урон +синего зелья**, если вдруг захочется полной совместимости; остальное только +съест банк. Если решим делать — это отдельная фича «уровень 15», а не часть +основного прохождения. + +--- + +## 5. Уровень 0 — демо-уровень (аттракт) + +`res2000.bin` у нас распакован. Это тот самый ролик, который крутится на +титульном экране: Кид сам бежит, дерётся со стражем и убегает. + +Как устроено: + +| место | что | +|---|---| +| `do_demo`, seg006:1409 | на уровне 0 вместо чтения клавиатуры зовётся `do_demo()` + `control()` | +| `autocontrol_kid`, seg002:702 | Кид управляется тем же `autocontrol_guard` | +| `do_auto_moves`, seg002:1089 | проигрыватель ЗАПИСИ ходов: таблица `(time, move)` | +| `on_guard_killed`, seg006:1928 | на уровне 0 после убийства стража Кид убегает (`checkpoint = 1`, сброс демо) | +| `demo_index` / `demo_time` | позиция в записи; у нас уже объявлены в `guards.c:267` | + +**Хорошая новость:** движок автодвижений (`do_auto_moves`) у нас уже есть — +он нужен был тени на уровнях 4/5/6, и `demo_time`/`demo_index` объявлены там +же. То есть демо-уровень — это в основном таблица ходов + ветка «Кидом +управляет ИИ» в `pop_ctrl`. + +**Когда делать:** вместе с интро/титульным экраном, не раньше. На +прохождение не влияет. + +--- + +## 6. Порядок работ + +Порядок задан пользователем 2026-08-13: **строго по номерам уровней**, а не +по дешевизне кода — уровень 13 не имеет смысла раньше, чем на него можно +попасть. + +1. ✅ **Уровень 12** — тень: `init_shad_12`, `autocontrol_shadow_level12`, + `united_with_shadow`, общий урон, «убил тень — убил себя», + `sword_disappears`, появление плит. Сделано 2026-08-13, хост-набор + `roomtest/tests-host/t_shadow.c` (45 проверок); живой проверки в MAME + ещё не было — доска [`L12-SHADOW`](../roomtest/TASKS_OPEN.md#l12-shadow). +2. ✅ **Переход 12 → 13** (§1.7) — room-триггер вместо двери и флаг + `pop_seamless` (не сбрасывать HP). Сделано там же. +3. ✅ **Уровень 13** — Джафар: `guard_notice_timer`, + `on_guard_killed`/`Jaffar_exit`, три спецкейса loose (§2.4) и + `check_fall_flo`. Плюс СПРАЙТЫ визиря (VIZIER.DAT) — без них он + рисовался обычным стражем. Сделано 2026-08-13, хост-набор + `roomtest/tests-host/t_jaffar.c` (44 проверки); живой проверки в MAME ещё + не было — доска [`L13-JAFFAR`](../roomtest/TASKS_OPEN.md#l13-jaffar). +4. **Уровень 14** — довести до комнаты 5 и поставить заглушку вместо ролика; + сам ролик — в общую задачу «катсцены». +5. **Уровень 0 и 15** — отложить: аттракт и защита от копирования на + прохождение не влияют. + +--- + +## 7. Сверка констант с ОРИГИНАЛЬНЫМИ данными (сделана 2026-08-13) + +Всё ниже снято скриптом прямо с `../SDLPoP/data/LEVELS/res2013.bin` и +`res2012.bin` — не из головы и не из констант SDLPoP. + +### 7.1 Падающие плиты уровня 13 — константы сходятся ТОЧНО + +Комнаты, у которых в ряду 2 вообще есть loose: + +| комната | loose в колонках | комната снизу | +|---|---|---| +| 17 | 2, 3, 4, 5, 6, 7 | **23** | +| 1 | 2, 3,    5, 6, 7 | **16** | +| 14 | 2 | 24 | + +Связи: `комната 23: up = 17`, `комната 16: up = 1`. То есть +`loose_tiles_room_1 = 23` и `room_2 = 16` — это ровно те две комнаты, над +которыми лежит ПОЛНАЯ гряда плит, а `first_tile = 22, last_tile = 27` — ровно +ряд 2, колонки 2..7: + +``` +комната 23 → сверху 17: 22:LOOSE 23:LOOSE 24:LOOSE 25:LOOSE 26:LOOSE 27:LOOSE [6/6] +комната 16 → сверху 1: 22:LOOSE 23:LOOSE 24:empty 25:LOOSE 26:LOOSE 27:LOOSE [5/6] +``` + +Два вывода: + +* диапазон 22..27 — **надмножество**: в комнате 1 тайл 24 пустой, и + `make_loose_fall` его молча пропустит (проверяет тип тайла). Копировать + константы можно как есть; +* пара «комната 14 → 24» НЕ входит в спецсобытие намеренно — одна плита, это + обычный loose. + +**Важное следствие, которого не было в плане:** стартовая комната уровня 13 — +**23** (`BP_START = 23`, поз 19, dir 0), а `check_fall_flo` зовётся из +`draw_level_first` → `check_the_end` (seg003:217). Значит плиты начинают +сыпаться СРАЗУ при входе на уровень, это его первый кадр, а не событие +где-то в середине. + +### 7.2 Джафар — один страж со skill 9 + +В `res2013.bin` заполнен ровно один слот стража: + +| комната | тайл | ряд, кол | направление | skill | +|---|---|---|---|---| +| 1 | 7 | 0, 7 | 255 (влево) | **9** | + +И это сходится с `meet_Jaffar`: событие срабатывает, когда Кид уходит ВПРАВО +из комнаты 3, а `комната 3: right = 1` — то есть ровно туда, где стоит +Джафар. Skill 9 у нас поддержан: `NUM_GUARD_SKILLS = 12`, все шесть таблиц +вероятностей (`guards.c:411..415`) имеют индекс 9. HP = `tbl_guard_hp[13] = 6` +(в `pop_level_cold.c` уже стоит). + +`Jaffar_exit` тоже проверен: **комната 24, tilepos 0 = `opener`** (кнопка), +модификатор 0 — то есть `trigger_button(0,0,-1)` жмёт реальную кнопку, а не +пустой тайл. + +### 7.3 Уровень 12 — меч на месте + +`комната 15, tilepos 1 = SWORD` — условие подъёма тени +(`get_tile(15,1,0) == tiles_22_sword`) на наших данных выполняется. +`комната 18: right = 19` — `sword_disappears` срабатывает при уходе вправо. + +Комната 15 целиком (по ней видно всю сцену встречи с тенью): + +``` +ряд 0: floor SWORD floor torch torch LOOSE LOOSE floor empty empty +ряд 1: wall wall wall empty empty empty empty wall floor floor +ряд 2: wall opener pillar empty empty empty empty wall pillar empty +``` + +### 7.4 `pop_loose_tick` действительно убьёт отрицательную фазу + +Подтверждено чтением кода: `m = ++pop_loose_modif[pos]; if (m & 0x80) { if (m +>= 0x84) → сброс в 0 }`. Для стартового `0xF0..0xFF` первый же тик даёт +`m >= 0x84` → фаза обнуляется, плита не падает. **Это и есть та правка №1 из +таблицы §2.4**, и она обязательна. + +### 7.5 `init_shad_12` и байтовое переполнение `y` + +`init_shad_12 = {0x0F, 0x51, 0xE8, 0, 0, 0, 0, 0}` → frame 15, x 81, +**y 232**, вправо, колонка 0, ряд 0, action 0 + `seq 7 (fall)`. Поле `y` в +`char_type` — `byte` (беззнаковое), то есть 232 лежит НИЖЕ поля (192), и +падение уводит его дальше с переполнением байта: тень «падает» из-под экрана и +появляется сверху. У нас `pop_char_t.y` тоже `uint8_t` — поведение +переносится без правок; при портировании просто не «чинить» это как ошибку. + +Отдельно про ряд: `curr_row` берётся ИЗ ТАБЛИЦЫ и равен **0**, хотя +`pop_y_to_row(232)` дал бы −1. `do_init_shad` копирует семь полей как есть и +ряд не пересчитывает, так что «согласовать» их — значит разойтись с +оригиналом. Закреплено тестом `shadow12_rises_when_sword_gone`. diff --git a/applications/PoP/poc/res/vizier/g0.atl b/applications/PoP/poc/res/vizier/g0.atl new file mode 100644 index 0000000000000000000000000000000000000000..4a8b0bd043ffa2ee80b4e898d382edfa0174ede1 GIT binary patch literal 9711 zcmdU!O^)0)5QWc@6GK?sjTvOd0|)Xhh!{maikJ=`3=efjI~>g9_Uhr{28yIY?B{O-+Lp8vRc|BmOo z@9)*V`QhO;+rQr4tN*tj-+W-3<<;TkVVaH;%7DM)fa)*|s0;)DhT-mT3!)Ta{jw7A z(40Uj7C%{CN6o_KVr@~PF{vh?rgw7Ad4vsO*C!t82vV@u|Kb=%(2YO*s{)*sb$lYWVc>P}3{v8Mh!Jx~%$w5M@C3p%Y2EO~`~4 zoJEwIp*oT?bRvmuL!q4z7Yz}8b4#uUECJkx&|vk=;cfNZZo>bQ<{4#us>85_;C2qj zp3GBLCPI6dmzwD#nWAg3^f}C>noN-y2I*X{nC3cj9mi=pUjXG9N0|u|n#OUw zK7zUQ7`qXW8OIYDrqegfWGp;ga3(UI#gJm+$XqfrIs>uM2vejwAmtiIwfcvYY!@^2 zHP{OsOSaDF%C5N6O9eUHXo|v-;hhSxx((J=DItP?UOpXSXqUQ3uk>q+eyJ8I%_98kCr! zw!Z3v%uKa)1DVvF>bD@w9av5R;V1kTNz6Ju&c$)D+2%YOU5;9ZJFK97L=BrjfCxziB$w^tMf-+TOM;s_kssgpQkz z0cMA5Uclr9+$PHHT~J)WdE~6ZZ1?}!6X(pLX7j@;UU-g-w`q|HkU7shLA>8P7%axQ z272P>_C<{BfYw~*+~|nFVTDPEGiH@EkTB=OSQi|CPFXVv@nKy4W|Tq485fQ|nR6ME zo3brEafbQy7<=N(xf-N?;&iHICMC;08jL$n#~*<+gJo9T&BaX4-LHWZvoWQcV`?QPmeUW zGA=!Q4}6rMd-2DD0LbEW;8P2Mkq@p#p##uJdjR)FDadKl9EyN?3Xqmx0@45xNP%Kq z0#X#`02omK^C1EzU>cSIBWbNNp$I_307V@DoE0#SEx2bXqYWAWoz>U#e6u8hS~|alUqWqaYB8fL;|Vl^QtN z%cKo)l_-$jHmf`-3_E4Vkm$R9PJ-Opykx%6xKbA0R zPT-_BK)4kj%WEb;bbkoZ+?$QI2+6R?;HO~yHlc*#Gh}lRgx@c?4`j%hyzZ3_q<-b# z6;nx&BU;-gWnRo+Vv37l`5EN)BdW1mN?E=LDOnKWLZ;N!A)2NC(vzp8NkQs5B`;fo z0I+a93o{R*dl-FAVa!r>I~zuB zSd(dUFW3b4L;R1-54&D9Hk10bZ_h4+UfDS972L(a1$J<9jLi#nU)h+xQ{kz-J0N5B zY`(GkBsI8UX9EgbkS}vO>wFIin~-tx>De}vF&dgCFVp8vqz%Nc2W?%1d1K+Y8Hp literal 0 HcmV?d00001 diff --git a/applications/PoP/poc/res/vizier/g1.atl b/applications/PoP/poc/res/vizier/g1.atl new file mode 100644 index 0000000000000000000000000000000000000000..39fbb21970e4c383c604dc9fc5c3c02498c25428 GIT binary patch literal 10732 zcmd6tO^)L>5QRO90LdoEBAWn>Y>fE@vd9gR8w8k5uGOAtw+#!>1?GNPD)nAfkwuD> z*oY?>Ei8)l=@rRtK9$`4@vlFB|Nijxk1vOZcduR@4nKYO@D}I4e|Uey`JW$;Z*czY z=i_Uf|M*4EAAdcP`*QriI7awzcz0Mfj_Y+Jm_ygh-EwbeozJ*<)Pw!ECRlJnFE?=W z%v-EaDph9fcnZ*vb(Xf@U7rn;Nz7>0VZTN! z9^PM_Bf@qS=VfdAuegpuRos?8CDd)~w6zsDT9s>Vg<@vp_Aw+Lw~``5??RHkKOFDt zBfVTN?N_&G+?73J2w{!1mdPqhM<)J6&brlWnTXIr0-&3=_<I%n6YOGo=n7A~W|r?W(gB zu4{YlqFW4NcBJH0!{ae8O@1<+`Mf?1}At>A0L&&}NL1_BHJFa%qV}fY&m)-g~UL5 zfb<*l*rfc4;t-5xtz}e%NV(DT4y|)NojI07#Ee3vt5Rv)iU|c%ONFk4i*a+aOGc5> zS8|qlpnOQFN)!qZ%3QfCh0zF>p$UO$^=e!o(A2?0?YWACOM%?l|0_i)d)kx+7xK9^ zV_0*SQYn8D6qtleZf!*j_`SxhDb#lvq8<#i=xSYD?@00ex0R&q8_LK+dGm$QMF$K( z&|wO$`FhATeE^&14NJ+JdJA%D!@;sYl#m+<2Z&wGM(L{N5H<%u*mtG%3KQyJLxhS- zav7LKWgt~SVX-n3I_Vzan!|yPSCfpD*9`L`+*k>ijF)a^;?C?&oCqy(Ihqa`p81+$+@r%Y=D@q4Fz*y zz7`et`X=OJnlIU58We=&umy$`nklReWgHTzSr|Dqs_py+?$_rkL5>ziQ;!tnU&o)&Ro*gLaNSoBIC4$<8AU0x&!ffwy4si&= zn2z`U6H2v0-_&J);nKFncq3NSjENHKkRMi2}ZFV zwlZc{E5%fbRbR@5hEav>ND<<1+Ftq>yxK|TzAY1xzb_WSR-WTRS@|K&xpx=D8Z)&ml*pvOI_Jc?Dz)5&=ilMZ`qkZQ%X zZ;U#H??3xmIu0FHMHNp1#FTOuRazX#9|Sh179}R?U%5ugrRvAPq7RFwCu)@x{a99j z|5_k%e+o1xJOrY>3tdv`#+%Oo%yRz`kSTY{!d>kGl&0LG=+8(w3*M5^J_%;~?{c78 zN2%K)tS^Hb5JGuWn;@{F`fk#I_9-v7n-aOxit^miZm}xg_Z`7To*_ff3m!+yzsmB3 A`Tzg` literal 0 HcmV?d00001 diff --git a/applications/PoP/poc/res/vizier/g2.atl b/applications/PoP/poc/res/vizier/g2.atl new file mode 100644 index 0000000000000000000000000000000000000000..5c8405c04be04b80953b6c118c6f18b1f4b47797 GIT binary patch literal 9865 zcmd6tOKu!D5QfLA1X*R_1PH7EhG4^h76x*J+#o*<$_8q5b=Bzy9*=;mfDL4>#{#yf_?w{O09*=Kp?o^M?6f-(SCF z{^t*`r2n6vt|b5O=hxEz$Mu!;bA~sEcZcQKrpwsOE@Rvnb{WDz^_|{I>SBuU8DgiC zu6_*5)a|kab2Dohi(jo`iAvBsry>itaKb0jLE`QjFl88siEQCQj*BjHL%iq0&;t zD^{C`reH5XFwHF!B$%6}5OdO1Jw`cojG^U#%2Hd&VB6Ey zjcl7&b>&vgZNngzR@9Df>SC^Gi>h}>QG3%ih%IB*h=mf;cm+3vK|cZoX^l~4`WomX zgnD^+|BSyhKGkQQ<4rb7oG0wxi`;Tqpahj5Yaey*bTaj0#%BC2sV=I^%DIj3Odo))>zL&qjD}c0B(^M6JZPE8G{vwF)Ip%RN~At5 zqn72vN6~l>I+bFL?mozYb0YIF4dZ~30%J{+V9ldJ($``+FE@ubhlO9-z4TPfg59_0 zi@UGSP*?DG?Lwo>jv-aLzB5z97}EXciWxCV$YD@c#Cp55mI-Fo}k+9PL3KBh6Ypg7$1W~xSb zloMtWhSW4UVrIyQuW72HM=+g@o*dOs#Ykm@0Vo((q0Tx1PKeIt9R`NxWm_rGRiyR2 z*-`*fjHs5edWmizmB}9C3+*xr?VY?htvv>o22OAz=Q1%PN1x8rAqXZL+t4VpZNYlP z)Aix)7dwXyd_fTY`=6Pw4Vgf+FR=dO#ZdFt`*f(;2b7pJA6A&!6SuS(C4f9?OVYGr zc;x`OnYBaCZfi!|K}mRoL|kNV*_X|ciiDFR;tO4^AyYTeHvFD1Ava_?X^O0>4O3#t z7?)CLvZ}4iHlc!Hke>mGyF$0!)$OAY3YjTy3G)1z2s*tfep$O(zRo_34sV(Rm@ zcEdCbc?uj^lM~D%YOP^4#c^N)!Au+xhw5O_p7KdcO|X>RR+e9=L3`6wZ}FzntAt)X z_5J4FGR=7h(~T%x_&!u0({|nZnJrkNM|8 zc?>hyEijoIj1~8E3RIMau>@_prmHcZEpowlPp6x#aHdyqhKf`Cit~FqaDI4mp%7Bd zJ>&pbVRC?5Veqc5-I`$PbsbKATqI1lG&dtpfl?IAAbKfLGqMmX4uwP{6;`UEI(2Hx zo~UAmP^ygNiY%c7b5~hfVI1N>E{1!&v@U~&0{RT96IgDP)Bwh>+hw_Wt^y%Z6RzXa z1j3ZnKl%Q$T7gguJF_nkdXdO&UR@Y90s$v@#}YN^A|GvtEEk9(yd(xU1fs=afymUO zfBt&{0o?VbFAx!ijou0bFj*>Iorgi0r`h2R^aR4jFuY)HMi!bgjNK`f?&@+vq)_)2 zwHn*>+hAKhx6CZ6kK{kONAW z%XilB=7OE9QLm(~2XoljOq;NtXJ}m1{@IXjw?im}@iPSVP*P z%eUtSR54-PR=-0wjWu>cE@s%8Cbx}bs>`UUJ|h02Up)1&kt-tHBHeX_O$}0DkZS8q zO_4q$g2uDV$6xJy-P;)K(Pnu;_46UbquA|vW6bj3uDad?x<7UyGW-FQHN@7$fb``V zgzcQ$)Hx}|^dHIezaC^t`t>0nF11wZkqMuHvKLAy5~M=4(#`ftkqRisp`Ci*jCTXb zWc$J&R+EBN3Y-raULcg)x28~Y$lsOZEhSeG>$`{w)2oOzRL+dXZBms#tI(bnJ5U}t W1kvA4ki+6ooLivh34|q=l>Y(0*I3^G literal 0 HcmV?d00001 diff --git a/applications/PoP/poc/res/vizier/g3.atl b/applications/PoP/poc/res/vizier/g3.atl new file mode 100644 index 0000000000000000000000000000000000000000..6b6aff33cc6b47b0b46d7b22555b6eed79b11b9c GIT binary patch literal 3283 zcmchZJ#HH@5QQli0Sp9g3`n&ByKtK$xS!W_~1-FY09A(CE) zJT90*jyr%ur8|(V)c_yrncJ zD#L@Qtx~7ciDQ_NRskId281mm=v4@4&dMO<4NH;(ff|y#kStAPoEds-L8*T}X9y+5 zII^amc3~GXACo-p!gI(ID=Oymxu6?8ay`Aq2uxmbgQB^VhKK@Y4CQPK&AE=HbYdJ> zJq_f(Y?~aHDrXkK71L%>{HAm^d**u1S&4viCt0JcCYfFdu;*HwM)FkEF@lx%=pV(P zoIVXs;MPF1EF26@pxAnAb#~EgDx=T3<_E1zJ?gAd#s+rx;qfoEV22EjFHy>X`%05n zm5CfksemT?w946*TvcB#$L?@-%|IaHX+q$R5gWKI*b_m(B<9TNBpKLF>g7VC+(;{n);unjnoyx8 zt&Id*Tkx6%R$lp_}7qyIX9St-G?7eY16JueTPn zb#&V?ljJ5VSXnREm{qlwncY$ba9t(|pe=Ezl?_Hs|g{8_&48y#Nsq*N%+NCHZXBVy7g%%@Ft5b6(Dn+eMNw0iCV-yuP z=vhxPr4hH4T0kY5%71fEl}#ox=OZpSDH^b9fGTf2`{EUqGF34#8Ct&lR##OOxF*HR YE>%r-Vpo{%q@CWWrOPx# literal 0 HcmV?d00001 diff --git a/applications/PoP/poc/res/vizier/g4.atl b/applications/PoP/poc/res/vizier/g4.atl new file mode 100644 index 0000000000000000000000000000000000000000..36515a470e90e15d75784ff6d0e3eedb367db598 GIT binary patch literal 1194 zcmcJN&20iP5QPm_jub!voKgZEL`pkp!U)^3u(TDWCg;8Rad3c@IAjp=+s|)i$Lslc zKfP_k|Ho%I>^GYbzPHC6SH>aiL%)gZwRiqC3ga<~@qC_b{T{_T^aYf&CwA5cc^xs6 z)ngpBfLIDBk2k340g`uSPcE%Bm}1Y_3lcRV9LpHW8ybvz3^ZY@EQXXtY0Hr?J2jYg zA7et#NptN$K==u&e#U%B7ou7RTG(eRAdUnWv&41e)wPrqbJR6+JW_~lnGl(QW7tgt zHT&kLo2Gx;pP?V>IkhRIK_!(+RivUSC)U-f{IJgTHe zBn59w*S`|&=!V?jn(XcbHuOAi%W@8?h8+gLT)^jl7@X4ERPC- HB1`W-C(#_@ literal 0 HcmV?d00001 diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md index 2e3126d..6d92740 100644 --- a/applications/PoP/roomtest/BUGS_OPEN.md +++ b/applications/PoP/roomtest/BUGS_OPEN.md @@ -15,7 +15,7 @@ | ID | что | тип | статус | |----|-----|-----|--------| | [SPRITE-ZERO-W0](#sprite-zero-w0) | кадр персонажа изредка отдаёт спрайт 0x0 (залипание уже вылечено) | **редкий** | открыт: нужна трасса маппинга W0 | -| [GATE-FORE-KID](#gate-fore-kid) | Кид в проёме ворот виден поверх решётки | окклюзия | открыт: портирован только шовный случай | +| [GATE-FORE-KID](#gate-fore-kid) | Кид в проёме ворот виден поверх решётки | окклюзия | **фикс есть, ждёт проверки в MAME** (2026-08-13) | | [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить | | [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт | | [TORCH-ANIM-RIGHT](#torch-anim-right) | под запечённым пламенем застывают не только челюсти чомпера | **низкий** | открыт: на уровнях 1-4 такого соседства нет | @@ -23,6 +23,13 @@ | [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается | | [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз | | [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику | +| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** | +| [LOOSE-ROOM-CHANGE](#loose-room-change) | расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату | порт | закрыт 2026-08-13, кроме [LOOSE-SEAM-ANIM](#loose-seam-anim) | +| [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** | +| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 | +| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры | +| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки | +| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» | --- @@ -427,7 +434,31 @@ for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row) читать её из MAME. Симптом редкий (пользователь поймал дважды). -## GATE-FORE-KID. Кид, стоящий В ПРОЁМЕ ворот, виден ПОВЕРХ решётки +## GATE-FORE-KID. Кид, стоящий В ПРОЁМЕ ворот, виден ПОВЕРХ решётки — ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ + +> **Портировано 2026-08-13** (повтор нашёлся на уровне 10, комната 7, тайл +> (2,6) — подтверждено чтением памяти MAME: `Kid.room=7, curr_row=2, +> curr_col=6`, код тайла `0x04`). Сделано так: +> +> * **решение** — `pop_gate_over_char(ch)` в `pop_map.c`: персонаж стоит на +> тайле ворот текущей комнаты (колонки 0..8; девятая отсекается, там бары +> легли бы в соседнюю комнату — у оригинала это `Kid.room != room_R`); +> * **отрисовка** — `draw_gate_fore` в `pop_bg.c`, точный порт seg008:1153: +> только прозрачные куски 51 + ломти 52, без непрозрачного 50 и без +> хвостового `DOOR_FRAM_SLICE` (фон под барами уже нарисован). Зовётся +> ОДИН раз на персонажа за кадр, не в тайловом цикле; +> * **тесты** — `tests-host/t_char.c`: `char_gate_covers_char_standing_in_it`, +> `char_gate_ignores_neighbour_tiles`, `char_gate_ignores_other_room`, +> `char_gate_never_in_last_column`. Отрисовка в харнесс не линкуется, +> поэтому проверяется именно решение — ради этого оно и вынесено в карту. +> +> Расхождение с оригиналом (осознанное, в `docs/impl_diff.md`): оригинал +> спрашивает жёстко про `Kid`, потому что foretable у него одна на проход +> тайлов; у нас fore-проход идёт по персонажу, поэтому спрашиваем про того, +> кого рисуем — страж в проёме тоже уходит за решётку. +> +> Шовный случай (ворота у СОСЕДА слева) работал и раньше — он ниже. +> Банк 2 +601 Б, банк 3 +92 Б, резидент не изменился. Нашёл пользователь 2026-08-11 (уровень 5, комната 24, верхние ворота): Кид стоит на тайле ворот, а решётка рисуется ПОД ним — он виден целиком, @@ -465,3 +496,319 @@ if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row && **Проверять:** уровень 5, комната 24 — встать в проём верхних ворот (колонка 1 ряда 0) при частично поднятой решётке; прутья должны перекрывать Кида. Тот же случай — любые ворота на уровнях 1-3. + +--- + + +## GUARD-ENTRY-DEATH. Вход в комнату со стражем вплотную = мгновенная смерть — ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ + +**Наблюдение (пользователь, 2026-08-13).** В некоторых ситуациях (особенно +после телепортов) Кид, входя в комнату, сразу оказывается чуть ли не в одном +тайле со стражем и немедленно погибает: меч ещё в ножнах, а безоружному любой +укол смертелен. + +**Что в оригинале.** Стража при входе НЕ отбрасывает — проверено по SDLPoP: +`enter_guard` (seg002:0112) ставит его ровно в тайл из данных уровня +(`Char.x = level.guards_x[room-1]`), никаких проверок дистанции до Кида нет. +Ситуация «вошёл вплотную» возможна и там. Защит четыре: + +1. **`bump_into_opponent` (seg003:08AA)** — главная. Безоружный Кид + (`Char.sword == sword_0_sheathed`) при вооружённом сопернике + (`Opp.sword != sheathed`, `Opp.action < 2`), лицом к лицу + (`Char.direction != Opp.direction`) и `can_guard_see_kid >= 2`, на + `ABS(char_opp_dist()) <= 15` не получает укол, а ОТСКАКИВАЕТ: `Char.y` + прижимается к полу, `fall_y = 0`, `seq_47_bump`. +2. Кид сам достаёт меч — `control_standing` (seg005:0351). +3. Страж поднимается в стойке покоя с мечом в ножнах — `seq_77`, ему нужно + сперва заметить Кида (`autocontrol_guard_inactive` → `move_down_forw`). +4. Вплотную страж не колет: `autocontrol_guard_active` при `distance < 12` + (или `< 8` с убранным мечом) разворачивается/шагает, а `check_hurting` + требует `min_hurt_range = 8` для безоружного противника. + +**Корень у нас.** Пункты 2-4 были портированы верно (`pop_ctrl.c:288`, +`pop_guard_cold.c:107`, `guards.c:553`), а **пункт 1 не портирован вовсе**: +в `kid_phys` (`pop_map.c`) между `determine_col()` и `check_collisions()` +было пусто, тогда как `play_kid_frame` (seg000:1238) зовёт там +`bump_into_opponent`. Без отскока дистанция успевает вырасти в рабочий +диапазон удара 8..29 — и первый же `move_6_shift` стража даёт +`hurt_by_sword` с `take_hp(100)`. + +**Сделано.** `bump_into_opponent` портирован в `pop_map.c` (рядом с +`bumped_floor`) и вызывается из `kid_phys` на штатном месте. Резидент не +вырос, банк 3: 11066 → 11156 Б. Опциональные фиксы SDLPoP +(`fix_painless_fall_on_guard`, `fix_jumping_over_guard`) намеренно НЕ +портированы — оставлено базовое поведение движка 1989 года. + +**Проверять:** войти в комнату со стражем вплотную (телепортом `+`/`−` или +`Shift+L`) без меча в руке — Кид должен отскочить с `seq_47`, а не умереть; +падение на стража сверху по-прежнему безболезненно (это оригинал, не баг). + +--- + + +## LOOSE-ROOM-CHANGE. Расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату — ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ + +**Наблюдение (пользователь, 2026-08-13).** Уровень 10, комната 2: в (2,9) +стоит плита, которая под бегом Кида должна разрушаться. Если Кид пробегает +по ней и уходит в правую комнату, плита не успевает разрушиться и упасть — +переключение комнаты обрывает алгоритм. + +**Что в оригинале.** loose-пол — обычный **trob** (`make_loose_fall`, +seg007:0EF6 → `add_trob(curr_room, curr_tilepos, 0)`), а `process_trobs` +(seg007:0000) крутит ВЕСЬ список каждый кадр, независимо от `drawn_room`: +`animate_tile` делает `get_room_address(trob.room)` и работает с таблицей +той комнаты, где trob живёт. Фаза лежит в `curr_room_modif`, то есть прямо +в таблице уровня. Поэтому брошенная плита досчитывает до +`loose_floor_delay`, `remove_loose` убирает тайл, `add_mob` рождает кусок — +и всё это без Кида в комнате. Ровно так же ведут себя `mobs[]` +(`do_mobs`, seg007:1063) — их список чистится только в `start_level`. + +**Корень у нас.** Падающие КУСКИ (mob) чужую комнату умели давно +(`pop_loose_mob_room_changed`, [BUG-LOOSE-2](BUGS_CLOSED.md)), а вот ФАЗА +тряски/отсчёта жила в `pop_loose_modif[30]` — массиве ТЕКУЩЕЙ комнаты, — +и `pop_loose_reset()` при смене комнаты обнулял его целиком. То есть +плита, брошенная на полпути, возвращалась в покой. То же для плит-потолков +(`pop_ceil_modif[10]`). + +**Сделано** (двухуровневое хранение вместо переноса всей механики в trob — +`pop_loose_modif` остаётся быстрым массивом отрисовки текущей комнаты): + +1. `pop_loose_leave_room()` (`pop_map.c`, зовётся первым делом в + `enter_room_side`, пока карта ещё описывает СТАРУЮ комнату) — сдаёт фазу + всех loose-тайлов в персистентный `room_modif` (это и есть + `curr_room_modif` оригинала) и заводит trob на незаконченные. +2. `animate_loose()` (`pop_trob.c`, case `TILE_LOOSE` в `pop_process_trobs`) + — досчитывает фазу заочно; на провале ставит тайл в EMPTY + (`pop_level_set_tile`), кладёт в модификатор тип уровня (`remove_loose`) + и рождает кусок через новый `pop_loose_mob_spawn_at(room, row, col)`. + Пока комната ОТРИСОВАНА, trob снимается сразу — хозяин фазы + `pop_loose_tick`. +3. `pop_loose_reset()` теперь не обнуляет, а ЗАБИРАЕТ фазу новой комнаты из + `room_modif`: вернувшийся Кид застаёт плиту на той же фазе. +4. `pop_loose_forget()` — старт/рестарт уровня: забыть фазы БЕЗ сдачи + (у оригинала всё сносит `load_level`). Без него первый же `enter_room` + после `pop_trob_reset()` занёс бы в свежий уровень дрожь из прошлой жизни. + +Резидент не изменился (20569 Б); банк 3 +451 Б, банк 6 +181 Б, банк 7 +62 Б. +Хост-тест `phys_loose_survives_room_change` (`tests-host/t_phys.c`) закрывает +передачу фазы. + +**Проверять:** уровень 10, комната 2 — пробежать по плите (2,9) в правую +комнату и вернуться: дыра должна быть на месте. Регресс: комната 7 +(две соседние плиты подряд), комната 12 плита (0,1) → уход в 15 +([BUG-LOOSE-2](BUGS_CLOSED.md)), плита-потолок, рестарт уровня после смерти +на дрожащей плите. + + +--- + + +## LOOSE-SEAM-ANIM. Плита в шве исчезает без анимации падения — МЕЛКИЙ, ОТЛОЖЕН + +> **Решение пользователя (2026-08-13):** «пока пусть будет так». + +**Наблюдение.** Уровень 10: Кид расшатывает плиту (2,9) комнаты 2 и убегает +вправо в комнату 7, где та же плита видна в шве как (2,−1). Плита +доваливается правильно (см. [LOOSE-ROOM-CHANGE](#loose-room-change)) и с +экрана исчезает, но **мгновенно** — без дрожания и без падающего куска. + +**Почему так.** Заочный досчёт (`animate_loose`, `pop_trob.c`) сознательно +не рисует: комната не отрисована, а фаза живёт в `room_modif`, мимо +`pop_loose_modif`, на который смотрит отрисовка дрожащего кадра. Исчезновение +видно только потому, что провал меняет ТАЙЛ, а по этому событию +(`pop_neigh_dirty`) главный цикл перечитывает кромку и перерисовывает шов. +Падающий кусок (mob) рождается в комнате 2 и честно летит, но `mob_tick_one` +рисует только `here` (своя комната), а комната 2 — не отрисованная. + +**Что надо, если браться.** Шов — это НЕ обычный тайл: чужая колонка 9 +рисуется внутри нашей колонки 0 (`draw_tile`, ветка `lcode == 11`), а +дрожащий кадр берётся из `pop_loose_modif[row*10 + (col-1)]`, то есть при +`col == 0` индекс уезжает за границу массива (для ряда 0 — в −1). Значит +анимация шва потребует не «включить рисование», а дать чужой колонке +собственный источник фазы. Заодно там же чинить и этот выход за границу. + +**Проверять:** тот же сценарий уровня 10; в оригинале плита в шве дрожит и +роняет кусок так же, как в своей комнате. + + +--- + + +## JUMP-FLOOR-LEGS. Прыжок с пола: ноги Кида поверх кромки пола — ПОЧИНЕН, ЖДЁТ ПРОВЕРКИ + +> **Корень (найден трассой 2026-08-13).** Проход оверлеев в +> `pop_fore_over_char` обходил **только два крайних ряда** футпринта: +> +> ```c +> if (action != 2) other_overlay_tile(rB, c); /* опорный */ +> if (rT != rB) other_overlay_tile(rT, c); /* верхний */ +> ``` +> +> Это буквальный порт `redraw_at_char2` (seg003:0645), и в оригинале он +> верен: там `char_top_row` и `char_bottom_row` ВСЕГДА соседние. У нас между +> ними появляется дыра — `char_footprint` форсит `rT = rB − 1` (голова торчит +> в ряд выше), а затем окно fore-клипа расширяет `rB` ещё на ряд вниз. +> +> На спорном кадре трасса из MAME дала `rT=0 rB=2 cL=0 cR=1 do_ov=1` и **ноль** +> вызовов `overlay_mid_tile`: ряды 0 и 2 обошли, ряд 1 — тот самый, где стоит +> плита пола, — пропустили. Отсюда и подсказка пользователя «глубже в падении +> плита рисуется правильно»: там `y_to_row` даёт уже 2, ряд 1 попадает в пару +> крайних, и всё сходится. +> +> **Фикс:** обходить ВЕСЬ диапазон `rB..rT`, сохранив правило оригинала «при +> `action == 2` опорный ряд не трогаем». После фикса трасса на том же кадре: +> один вызов `overlay_mid_tile`, исход для (1,1) = «нарисовали», коды тайлов +> `curr=3 left=0` — байт в байт как в отладочном выводе SDLPoP. +> +> Пилларный оверлей, который «работал и раньше», к этому отношения не имел: +> это `fore_id = 95` из отдельного fore-прохода, а не midtable-оверлей. +> +> Банк 2 +28 Б. Хост-тесты (6 наборов) проходят. + +### Разбор, по которому искали (оставлен: метод пригодится) + +**Наблюдение (пользователь, 2026-08-13).** Уровень 10, комната 1, прыжок с +пола. В оригинале нижняя часть Кида скрыта ЗА передней гранью пола, у нас +рисуется весь Кид поверх неё (за передней колонной он при этом уходит +правильно). Скриншоты — в переписке сессии. + +**Состояние кадра (снято из памяти MAME на замороженном кадре, сборка +2026-08-13):** + +| поле | значение | +|------|----------| +| `Kid.frame` | 103 (`frame_103_start_fall_2`) | +| `Kid.action` | 3 (`actions_3_in_midair`) | +| `Kid.x` / `Kid.y` | 69 / 127 | +| `Kid.direction` | −1 (влево) | +| `Kid.curr_col` / `curr_row` | 0 / 2 | +| `Kid.room` / `cur_room` | 1 / 1 | + +`pop_y_land[2] = 118` — то есть ноги на 9 px НИЖЕ уровня пола ряда 1, ровно в +полосе его передней грани (`draw_tile_bottom` рисует её на `63*row + 65 = 128`). + +Тайлы комнаты 1 (`room_fg`, прочитаны там же): + +``` +ряд 0: 01 01 00 00 00 00 00 00 04 0B +ряд 1: 04 00 03 13 10 11 13 01 07 0F +ряд 2: 0C 01 03 01 01 0F 01 01 04 03 +``` + +Кромочная колонка левого соседа (`lcol_fg[0..2]`): `04 0C 04`. + +**Что УЖЕ проверено и исключено.** + +1. **Гейт `redraw_at_char2` (seg003:0645) портирован верно.** Сверено строка + в строку: наш `do_ov` в `pop_fore_over_char` даёт то же множество поз + (захват 78-79, `action` 2/3/4/6, `bumped` 102-106, начало подъёма 135-136), + и climb-ветка 137..144 тоже совпадает. При `action == 3` оверлей + ВКЛЮЧАЕТСЯ — дело не в гейте. +2. **`draw_other_overlay` (seg008:1492) на колонке 0 не сработал бы и в + оригинале.** Обе его ветки требуют пустого соседа: `tile_left == empty` + (у нас `lcol_fg[1] = 0x0C`, doortop — не пусто) либо `drawn_col > 0` и + пустой тайл ЧЕРЕЗ ОДИН слева. Значит окклюдер — НЕ он, и наш порт этой + функции здесь ни при чём. +3. **`pop_tile_code(row, −1)` кромку читает правильно** — из `pop_t_lfg`, а не + «стена всегда». + +**Две живые гипотезы, проверять в этом порядке.** + +* **Это вообще не отрисовка, а физика.** У нас `Kid.y = 127` при уровне пола + 118 — Кид «утоплен» на 9 px, и ноги торчат ниже кромки просто потому, что + они там и есть. Проверяется дёшево: снять `Char.y` у SDLPoP на том же + кадре 103 той же последовательности. Если там 118..120 — искать надо в + seqtbl/`fall_speed`, а не в слое фона. +* **Окклюдер — другой тайл/другой механизм.** Тогда нужна очная ставка: + прогнать SDLPoP с трассой (`dbg_shots`-fprintf в `draw_other_overlay` и + `add_kid_to_objtable` уже стоят в нашей копии, см. memory + `pop_pixel_diff_vs_sdlpop`) и сравнить СПИСОК тайлов, попавших в midtable + на этом кадре, с тем, что рисуем мы. Гадать дальше по коду смысла нет — + два прохода по seg008 уже не дали ответа. + +**Проверять после починки:** тот же прыжок в комнате 1 уровня 10; за передней +колонной Кид уходит и сейчас — это не должно сломаться. + + +--- + + +## MOB-CLIP-RIGHT. Окна и узор кладки рисуются поверх падающих плит + +**Найдено прогонами 2026-08-13 (уровень 13, комнаты 16 и 23).** + +**Симптом.** На падающей плите проступают элементы ЗАДНЕЙ стены — узор +кладки, а на ряде 0 и целое окно. Плита при этом цела: элементы ложатся +поверх неё, не стирая. + +**Корень.** `mob_render` (`pop_room.c`) каждый кадр вызывает +`draw_tile(r, m->col + 1)` — перерисовывает ЦЕЛЫЙ соседний тайл поверх уже +нарисованного куска, чтобы спрятать его правую часть (спрайт 42 лежит в +столбце `col+1`). `draw_tile` кладёт всё содержимое тайла, включая декор +пустого тайла — окно и узор. Ряд берётся из координаты куска, поэтому на +ряде 0 перерисовывается тайл с окном. + +Правка [#6](#) (клип перерисовки окном коридора heal) сделала артефакт +ЗАМЕТНЕЕ: раньше перерисовка размазывалась вокруг, теперь попадает точно в +прямоугольник плиты. + +**Как в оригинале.** Такой перерисовки нет вовсе. `redraw_at_cur_mob` +(seg007:1094) зовётся ТОЛЬКО из `loose_fall`, один раз в момент сбития, и +лишь ставит пометки на тайл и соседа. Правая часть куска прячется +собственным клипом объекта: `add_mob_to_objtable` (seg007:1161) задаёт +`clip.right = 40`. + +**Что делать.** Портировать клип вместо подпорки: + +1. выяснить, во что превращается `clip.right = 40` в наших координатах + (у оригинала `obj_x` в удвоенных единицах: `char_x_left = obj_x / 2 + 58`); +2. блитить правую часть (env 42) с ограничением ширины — примитив с шириной + есть для персонажей (`gfx_blit_cols_part_w`), для env надо посмотреть; +3. убрать `draw_tile(r, col+1)` и окно fore-клипа вокруг него; +4. вернуть коридор heal к 64x32. + +**Побочная выгода:** уходит по одному полному `draw_tile` на кусок за кадр — +при шести падающих плитах это шесть отрисовок тайла. + +--- + + +## MID-OVERLAY-LAYER. Оверлей кромки всегда поверх, а должен сортироваться + +**Найдено разбором 2026-08-13.** + +`overlay_mid_tile` (`pop_bg.c`) выполняется в проходе ПОСЛЕ персонажей, то +есть безусловно поверх всего нарисованного. В оригинале это `draw_other_overlay` +(seg008:1499), и он переключает таблицу на **midtable** — +тот же слой, где живут персонажи и падающие куски (`add_mob_to_objtable`, +тип `0x80`), а внутри слоя всё сортируется по `y` (`compare_curr_objs`, +seg008:1572). + +Следствие: узор из оверлея выигрывает у плиты, даже когда плита ближе к +зрителю. Пока у нас нет сортируемого midtable, точечными правками это не +лечится. + +Связано с [BG-ONCE](TASKS_OPEN.md#bg-once) — там та же техническая +сердцевина: привести наши слои в соответствие с таблицами оригинала. + +--- + + +## GUARD-FALLOUT-VICTORY. Выпавший из комнаты страж не засчитывается убитым + +**Найдено разбором 2026-08-13** (при проверке чита `K` на Джафаре). + +У `on_guard_killed` в оригинале ДВА места вызова: + +| место | что | +|---|---| +| `play_guard`, seg006:1494 | HP кончились — **портировано** | +| `check_guard_fallout`, seg002:264 | страж выпал из комнаты — **НЕ портировано** | + +Наш `pop_guard_fallout` (`pop_guard.c`) просто гасит слот. Значит столкнуть +Джафара в пропасть на 13-м уровне можно, а выход на 14-й от этого не +откроется — расхождение с оригиналом. + +Правка: позвать `on_guard_killed()` в ветке «не скелет». Мешает только то, +что функция сейчас `static` в `guards.c` (банк 1), а `pop_guard_fallout` +живёт в резиденте. diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 71a17c0..30127e2 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -94,8 +94,16 @@ SKEL_DIR := $(CURDIR)/../poc/res/skel # страж (tbl_guard_type[9] = 0). См. pop_pack_guard.py. SKEL_DATA := $(SKEL_DIR)/g0.atl $(SKEL_DIR)/g1.atl $(SKEL_DIR)/g2.atl \ $(SKEL_DIR)/g3.atl +# Джафар — тоже отдельный набор (VIZIER.DAT): его грузит уровень 13 +# (tbl_guard_type == 3). Кадров столько же, сколько у стража (34), но +# палитра своя, из файла набора: curr_guard_color для не-стражей = 0. +# Зеркального набора нет по той же причине, что у скелета. +VIZIER_DIR := $(CURDIR)/../poc/res/vizier +VIZIER_DATA := $(VIZIER_DIR)/g0.atl $(VIZIER_DIR)/g1.atl $(VIZIER_DIR)/g2.atl \ + $(VIZIER_DIR)/g3.atl $(VIZIER_DIR)/g4.atl -EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) $(LVL_DATA) +EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \ + $(VIZIER_DATA) $(LVL_DATA) include $(PROJ_ROOT)/app.mk # Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS), @@ -111,6 +119,7 @@ hdd: $(EXAMPLE).exe $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(SKEL_DATA),SKEL:$(f)) \ + $(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \ $(foreach f,$(LVL_DATA),LEVELS:$(f)) TC := $(PROJ_ROOT)/applications/PoP/toolchain @@ -122,6 +131,9 @@ $(GUARD_DATA): $(TC)/pop_pack_guard.py $(SKEL_DATA): $(TC)/pop_pack_guard.py cd $(TC) && python3 pop_pack_guard.py SKEL +$(VIZIER_DATA): $(TC)/pop_pack_guard.py + cd $(TC) && python3 pop_pack_guard.py VIZIER + $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py cd $(TC) && python3 pop_pack_kid.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index f7b53c7..19edf14 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -231,6 +231,151 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS. ## P0 — делаем сейчас +### L12-SHADOW. Уровень 12: встреча с тенью, бой, слияние + +> **Статус 2026-08-13: код написан, хост-тесты зелёные (45 проверок, +> `tests-host/t_shadow.c`), ЖИВОЙ ПРОВЕРКИ В MAME ЕЩЁ НЕ БЫЛО.** Для неё +> нужна сборка с `FIRST_LEVEL=12` и полный рестарт MAME после `make hdd`. + +План и сверка констант с оригинальными данными — +[`../docs/levels_12_15_plan.md`](../docs/levels_12_15_plan.md) §1 и §7. +Сделано ровно по нему: + +| кусок | где | порт | +|---|---|---| +| подъём тени в комнате 15 (условие «меч уже подобран») | `guards.c` `pop_check_shadow` | seg002:0070 | +| `init_shad_12` + вход ПАДЕНИЕМ (`seq_7`) | `guards.c` | data:0EFA | +| ИИ тени: ждать Кида / драться / сойтись / слиться | `guards.c` `autocontrol_shadow_level12` | seg002:1184 | +| «ранил тень — ранил себя» | `pop_guard.c` `pop_do_delta_hp` | seg000:1518 | +| «убил тень — убил себя» | `guards.c` `pop_check_killed_shadow` | seg006:2033 | +| таймер вспышки слияния (0 → счётчик → −1) | `pop_guard.c` `pop_shadow_timer` | seg003:0735 | +| меч исчезает при уходе вправо из комнаты 18 | `guards.c` `pop_sword_disappears` | seg002:0536 | +| появление плит в комнатах 2/13 после слияния | `pop_map.c` `floors_appear` | seg006:1063 | +| бесшовный переход 12 → 13 (комната 23, без двери) | `roomtest.c` + `pop_start_level` | seg000:0900 | + +Осознанное расхождение: мигания Кида спрайтами тени во время вспышки нет — +запись в [`../docs/impl_diff.md`](../docs/impl_diff.md). + + +### BG-ONCE. Задний фон рисуется ОДИН раз; вместо чёрных баров — heal + +**Предложение пользователя 2026-08-13. Не делать сейчас — обязательно к +проверке и реализации на этапе полиша/оптимизации.** + +Суть: элементы САМОГО заднего фона — факелы без пламени, окна, узор кладки, +решётчатая кладка и что ещё найдётся — впечатываются в фон один раз и больше +не перерисовываются никогда. Ни один объект за ними оказаться не может, а +значит и возвращать их поверх чего-либо не нужно. Если такой элемент оказался +затёрт, это САМО ПО СЕБЕ сигнал об ошибке: либо бар/прямоугольник слишком +велик, либо что-то рисуется не в своём слое. + +Вторая половина предложения — и она про скорость: там, где мы гасим область +чёрным баром перед выводом спрайта, звать вместо бара **heal по координатам +бара**. Бар всё равно приходится потом закрывать фоном, то есть выходит две +операции; heal делает то же одной и сразу правильным содержимым. + +**Оценка: идея верная по направлению, но есть два известных препятствия.** + +1. **Узкий heal уже пробовали — и откатили.** В `pop_room.c` (`pop_clip_sprite`) + записано: per-спрайтовый heal не используется, потому что выравнивание + блока акселератора оставляет 1-2 px слайверы правой грани, и на одной + странице дабл-буфера это мерцает. Поэтому борта чистит полоса + `pop_room_clip_borders` с гейтом по флагу. Прежде чем менять бары на heal, + надо решить именно эту задачу — иначе получим мерцание вместо ускорения. +2. **Полный запрет «не перерисовывать фон» противоречит foretable.** + `draw_tile_fore` (seg008:715) кладёт в передний слой ГЛАВНУЮ ГРАНЬ стены + вместе с её узором — она обязана перекрывать персонажа. То есть запрет + должен различать «декор задней стены» и «передняя грань», а это ровно + различие backtable/foretable оригинала. Фактически предложение сводится к + «привести наши слои в соответствие с таблицами оригинала» — тот же вывод, + к которому пришёл разбор `overlay_mid_tile` (см. ниже). + +**Известное исключение**, которое надо не сломать: Кид, уходящий по лестнице +в дверь уровня, действительно уезжает за передний план — но он и рисуется +отдельно (`clip_char`, правая грань doortop; см. memory `pop_clip_char_todo`). + +**Критерий приёмки этапа:** пройти уровни с падающими плитами (13) и с +факелами/окнами (10-12) и убедиться, что ни один фоновый элемент не +перерисовывается в кадре, где его тайл не менялся; замерить кадр до и после. + + +### L12-SHADOW-SPR. Тень дерётся спрайтами СТРАЖА, а не своими + +**Найдено прогоном 2026-08-13.** `tbl_guard_type[12] = 4` — это SHADOW.DAT, +отдельный набор спрайтов (силуэт Кида), а `pop_guard_load` (`pop_cdraw.c`) +разбирает только тип 2 (SKEL) и 3 (VIZIER); тип 4 сваливается в набор +обычного стража. Ассеты есть: `SDLPoP/data/SHADOW` (32 кадра + `res750.pal`). + +Правка по образцу VIZIER (набор в `pop_pack_guard.py`, каталог в Makefile, +ветка в `pop_guard_load`), НО сначала проверить две вещи, иначе спрайты +поедут: (1) какую таблицу кадров получает `CHARID_1_SHADOW` в +`pop_frame_tbl_is_guard` — Кида или стража, и (2) сколько кадров в наборе +(32 против 34 у стража) и не выходит ли индекс за его пределы. + +Это НЕ полный «вид тени» (OR/XOR-блиттеры, [`../docs/shadow_render.md`]) — +только правильный набор спрайтов вместо чужого. + + +### L12-SEAM-POLISH. Переход 12 -> 13 выглядит коряво + +Механика работает (комната 23, без заставки и без сброса HP), но сама +анимация прохода рваная — замечено пользователем 2026-08-13. Полишинг, +берётся вместе с остальной шлифовкой переходов. + + +### L13-JAFFAR. Уровень 13: Джафар, победа, выход, падающая гряда + +> **Статус 2026-08-13: код написан, хост-тесты зелёные (44 проверки, +> `tests-host/t_jaffar.c`), ЖИВОЙ ПРОВЕРКИ В MAME ЕЩЁ НЕ БЫЛО.** + +Своего ИИ у Джафара нет — это обычный страж (seg002:085A), skill 9 и 6 HP уже +лежат в `pop_level_cold.c`. Сделаны только события и данные: + +| кусок | где | порт | +|---|---|---| +| фора после встречи (`guard_notice_timer`) | `guards.c` `pop_meet_jaffar` + `autocontrol_guard_inactive` | seg002:0544 / 0736 | +| победа: белая вспышка + «выход разрешён» | `guards.c` `on_guard_killed` | seg006:1927 | +| выход: кнопка комнаты 24 при уходе ВЛЕВО | `guards.c` `pop_jaffar_exit` | seg002:0517 | +| гряда плит сверху при входе в комнаты 23/16 | `pop_map.c` `pop_check_fall_flo` | seg000:1317 | +| фазу со старшим битом на 13-м НЕ гасить | `pop_map.c` `pop_loose_tick`, `pop_trob.c` `animate_loose` | seg007:823 | +| сотрясение на 13-м плиты НЕ трясёт | `pop_map.c` `do_knock` | seg007:949 | +| плита бьёт и В БЕГЕ | `pop_map.c` `fell_on_your_head` | seg007:1218 | +| СПРАЙТЫ Джафара (VIZIER.DAT, своя палитра) | `pop_pack_guard.py`, `pop_cdraw.c`, `Makefile` | seg000:1092 | + +По ходу нашлась и починена смежная ошибка: цвет стража из данных комнаты +применялся к ЛЮБОМУ набору, хотя оригинал берёт его только у обычного стража +(`curr_guard_color = 0` при `tbl_guard_type != 0`, seg002:183) — скелета это +не задевало случайно, Джафара покрасило бы. + +Осознанное расхождение (представление фазы отложенного старта) — запись в +[`../docs/impl_diff.md`](../docs/impl_diff.md). + +**Не портировано намеренно:** `is_show_time` в `on_guard_killed` — это показ +ОСТАВШЕГОСЯ ВРЕМЕНИ, а часов на 60 минут у нас пока нет вовсе. + +**Что проверять в MAME** (`make LEVEL=13`): + +1. комната 23 (стартовая): плиты сверху сыплются ВРАЗНОБОЙ с первых кадров, + и они бьют, даже если пробегать под ними; +2. приземление Кида плиты НЕ трясёт (иначе гряда посыплется разом); +3. уход вправо из комнаты 3: Джафар виден СВОИМИ спрайтами и своей палитрой, + ~2 секунды стоит, не доставая клинок; +4. смерть Джафара — белая вспышка; уход ВЛЕВО открывает дверь уровня. + +**Что проверять в MAME** (по порядку прохождения уровня): + +1. комната 15: пока меч лежит — тени нет; подобрал меч, вышел и вернулся — + тень СВАЛИВАЕТСЯ сверху, а не стоит готовая; +2. бой: каждый удар по тени отнимает HP и у Кида; добить её нельзя — на её + смерти умирает и Кид (белая вспышка); +3. слияние: убрать меч (`Shift`+вниз), подойти — белая вспышка, тень исчезла, + потолок HP вырос на единицу; +4. после слияния в комнатах 2 и 13 (верхний ряд) под ногами появляются плиты; +5. уход ВПРАВО из комнаты 18 убирает меч из комнаты 15 (вернуться и взять + второй раз нельзя); +6. комната 23 — уровень молча становится 13-м, HP НЕ сбрасывается. + + ### L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1) > **Статус 2026-08-12: код написан, ждёт живой проверки в MAME.** Сделаны все diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index ee35b36..c766fe7 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -29,6 +29,7 @@ #define TILE_MIDX 7 /* середина тайла в координатах персонажа */ #define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */ +#define TILE_FLOOR 1 #define TILE_SPIKE 2 #define TILE_GATE 4 #define TILE_DOORTOP_FLOOR 7 @@ -270,18 +271,28 @@ static uint8_t demo_index; /* текущая запись #define SHADOW_SKILL 3 #define SHADOW_HP 4 #define SEQ_2_STAND 2 +#define SEQ_7_FALL 7 #define TILE_POTION 10 +#define TILE_SWORD 22 /* Таблицы init_shad_N (data:0EEA/0EF2) — ПЕРВЫЕ СЕМЬ полей Char в том же * порядке, в каком их копирует оригинал (`memcpy(&Char, source, 7)`): * frame, x, y, direction, curr_col, curr_row, action. * 5: x 55, y 55, вправо, колонка −1 (ещё за левым краем экрана), ряд 0; * 6: x 81, y 118, вправо, колонка 0, ряд 1 — тень стоит на границе - * (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту. */ -static const uint8_t init_shad_5[7] = { 0x0F, 0x37, 0x37, 0, 0xFF, 0, 0 }; -static const uint8_t init_shad_6[7] = { 0x0F, 0x51, 0x76, 0, 0x00, 1, 0 }; + * (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту; + * 12: x 81, y 232, вправо, колонка 0, ряд 0 — и это НЕ ошибка данных: 232 + * лежит НИЖЕ поля (192), поле y беззнаковое, и тень с seq_7 «падает» + * из-под комнаты, переполняя байт, то есть влетает в кадр сверху. + * Ряд при этом берётся ИЗ ТАБЛИЦЫ (0), а не пересчитывается из y + * (иначе был бы −1): семь полей копируются как есть, и «поправить» + * их согласованность нельзя (см. ../docs/levels_12_15_plan.md §7.5). */ +static const uint8_t init_shad_5[7] = { 0x0F, 0x37, 0x37, 0, 0xFF, 0, 0 }; +static const uint8_t init_shad_6[7] = { 0x0F, 0x51, 0x76, 0, 0x00, 1, 0 }; +static const uint8_t init_shad_12[7] = { 0x0F, 0x51, 0xE8, 0, 0x00, 0, 0 }; -static void do_init_shad(const uint8_t *tbl, uint8_t room) +/* seq — поза, с которой тень входит в сцену: 5/6 встают в стойку, 12 падает. */ +static void do_init_shad(const uint8_t *tbl, uint8_t room, uint8_t seq) { pop_loadshad(); Char.frame = tbl[0]; @@ -296,7 +307,7 @@ static void do_init_shad(const uint8_t *tbl, uint8_t room) Char.alive = Kid.alive; /* наследование от Кида, см. шапку */ Char.sword = Kid.sword; Char.fall_x = Char.fall_y = 0; - pop_char_set_seq(SEQ_2_STAND); + pop_char_set_seq(seq); pop_saveshad(); demo_time = 0; @@ -309,6 +320,25 @@ static void do_init_shad(const uint8_t *tbl, uint8_t room) uint8_t pop_check_shadow(uint8_t room) __banked { + /* Уровень 12 (seg002:0070) — встреча с тенью в комнате 15. Три отличия + * от 5/6, и все три существенные: + * — событие одноразовое: после слияния (pop_united_shadow != 0) тень + * больше не поднимается ни при каком входе в комнату; + * — условие на тайл ОБРАТНОЕ уровню 5: там тень поднимается, ПОКА + * зелье на месте, здесь — только когда меча (15,1,0) уже НЕТ, то + * есть Кид его подобрал. Меч на месте — return 0, и это не «ничего + * не делать»: стража в комнате 15 в данных нет, так что вызывающий + * спокойно идёт в pop_guard_enter и убирает слот; + * — поза входа не стойка, а ПАДЕНИЕ (seq 7). + * pop_shadow_init сбрасываем здесь же: тень начинает «ждать Кида» + * заново при каждом подъёме (см. autocontrol_shadow_level12). */ + if (pop_current_level == SHADOW_FIGHT_LEVEL) { + if (pop_united_shadow || room != SHADOW_FIGHT_ROOM) return 0; + if (pop_level_tile(room, SHADOW_SWORD_TILEPOS) == TILE_SWORD) return 0; + pop_shadow_init = 0; + do_init_shad(init_shad_12, room, SEQ_7_FALL); + return 1; + } /* Уровень 6 (seg002:0090): условий на содержимое комнаты нет вовсе — * тень встаёт при КАЖДОМ входе в комнату 1. Оригинал здесь ещё играет * sound_25_presentation один раз, отмечая это `leveldoor_open = 0x4D` @@ -316,7 +346,7 @@ uint8_t pop_check_shadow(uint8_t room) __banked * переменную портить незачем — TODO вместе со звуком. */ if (pop_current_level == SHADOW_STEP_LEVEL) { if (room != SHADOW_STEP_ROOM) return 0; - do_init_shad(init_shad_6, room); + do_init_shad(init_shad_6, room, SEQ_2_STAND); return 1; } if (pop_current_level != SHADOW_STEAL_LEVEL) return 0; @@ -324,10 +354,108 @@ uint8_t pop_check_shadow(uint8_t room) __banked /* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража * комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */ if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1; - do_init_shad(init_shad_5, room); + do_init_shad(init_shad_5, room, SEQ_2_STAND); return 1; } +/* sword_disappears (seg002:0536) — спецсобытие уровня 12: меч, лежащий в + * комнате 15, пропадает с карты, как только Кид ушёл ВПРАВО из комнаты 18. + * Смысл событийный: дальше по уровню возврата за мечом уже нет, и сцена с + * тенью обязана быть однозначной — либо Кид пришёл к ней вооружённым, либо + * нет. Модификатор гасим вместе с тайлом (как SDLPoP): ненулевой modif на + * полу рисует «фальшивые» узоры. + * + * Звать при уходе ВПРАВО, ДО смены комнаты, — там же, где level3_set_chkp + * (оригинал держит оба в leave_room). */ +void pop_sword_disappears(void) __banked +{ + uint8_t *m; + if (pop_current_level != SHADOW_FIGHT_LEVEL) return; + if (Kid.room != SWORD_GONE_ROOM) return; + pop_level_set_tile(SHADOW_FIGHT_ROOM, SHADOW_SWORD_TILEPOS, TILE_FLOOR); + m = pop_trob_modif(SHADOW_FIGHT_ROOM); + if (m) m[SHADOW_SWORD_TILEPOS] = 0; +} + +/* meet_Jaffar (seg002:0544) — спецсобытие уровня 13: Кид уходит ВПРАВО из + * комнаты 3 и попадает прямо к Джафару. Всё событие — фора: пока + * pop_guard_notice_timer идёт, Джафар не встаёт в боевую стойку + * (autocontrol_guard_inactive), то есть у Кида есть ~2 секунды достать меч. + * В оригинале здесь же играет sound_29_meet_Jaffar — звука у нас нет. + * + * `pop_leveldoor_open == 0` в условии не косметика: вернувшись к Джафару + * после его смерти (дверь уже открыта), форы быть не должно. + * + * Звать при уходе ВПРАВО, ДО смены комнаты, — рядом с pop_sword_disappears + * (оригинал держит оба в одной ветке leave_room). */ +void pop_meet_jaffar(void) __banked +{ + if (pop_current_level != JAFFAR_LEVEL) return; + if (pop_leveldoor_open != 0) return; + if (Kid.room != JAFFAR_MEET_ROOM) return; + pop_guard_notice_timer = JAFFAR_NOTICE_TIME; +} + +/* Jaffar_exit (seg002:0517) — парная половина победы: смерть Джафара сама + * дверь НЕ открывает, она лишь ставит pop_leveldoor_open = 2. Дверь + * открывает КНОПКА комнаты 24 (tilepos 0 — настоящий opener, проверено по + * res2013.bin), «нажимаемая» в момент ухода Кида ВЛЕВО. + * + * Звать при уходе ВЛЕВО, ДО смены комнаты, — рядом с чекпойнтом уровня 3. */ +void pop_jaffar_exit(void) __banked +{ + uint8_t *m; + if (pop_leveldoor_open != 2) return; + m = pop_trob_modif(JAFFAR_EXIT_ROOM); + pop_trigger_button(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS, + pop_level_tile(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS), + m ? m[JAFFAR_EXIT_TILEPOS] : 0); +} + +/* on_guard_killed (seg006:1927) — соперник только что потерял последнее HP. + * Уровень 13 — единственный, где у этого есть последствия помимо звука: + * белая вспышка и pop_leveldoor_open = 2, то есть «выход разрешён» (сама + * дверь откроется кнопкой из pop_jaffar_exit). + * + * Ветку `is_show_time = 1` не портируем: это показ ОСТАВШЕГОСЯ ВРЕМЕНИ, а + * часов на 60 минут у нас пока нет вовсе — появятся вместе с таймером игры. + * Демо-уровень (0) сюда тоже не входит: его ветка — часть аттракта. */ +static void on_guard_killed(void) +{ + if (pop_current_level != JAFFAR_LEVEL) return; + pop_flash_color = POP_FLASH_WHITE; + pop_flash_time = JAFFAR_FLASH_TIME; + pop_leveldoor_open = 2; +} + +/* check_killed_shadow (seg006:2033) — «убил тень = убил себя». На уровне 12 + * Кид и тень связаны насмерть: как только один из них перестал быть живым + * (alive >= 0), второму отсчитывается 100 урона, то есть гарантированная + * смерть, и обоих накрывает белая вспышка. + * + * Условие оригинала `(Char.charid | Opp.charid) == charid_1_shadow` читается + * так: в паре ровно Кид (0) и тень (1) — со стражем или скелетом связи нет. + * Оригинал зовёт это ДВАЖДЫ за кадр (из тика Кида и из тика соперника, с + * уже переставленными окнами); нам хватает одного вызова на кадр с явным + * разбором обеих сторон — окна Char/Opp здесь не нужны, кроме как для + * маршрутизации урона внутри pop_take_hp. */ +void pop_check_killed_shadow(void) __banked +{ + if (pop_current_level != SHADOW_FIGHT_LEVEL) return; + if ((uint8_t)(Kid.charid | Guard.charid) != CHARID_1_SHADOW) return; + if (Kid.alive < 0 && Guard.alive >= 0) { + pop_flash_color = POP_FLASH_WHITE; + pop_flash_time = 5; + pop_loadkid(); + pop_take_hp(100); + } else if (Guard.alive < 0 && Kid.alive >= 0) { + pop_flash_color = POP_FLASH_WHITE; + pop_flash_time = 5; + pop_loadshad(); + pop_take_hp(100); + } +} + /* ---- Синтетический ввод (seg002:0706..) --------------------------- */ static void move_0_nothing(void) { @@ -399,8 +527,11 @@ static void autocontrol_guard_inactive(void) } } if (can_guard_see_kid) { - /* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */ - move_down_forw(); + /* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). + * Уровень 13: пока идёт фора после встречи (pop_meet_jaffar), Джафар + * стоит и ждёт — это его единственное отличие от прочих стражей. */ + if (pop_current_level != JAFFAR_LEVEL || pop_guard_notice_timer == 0) + move_down_forw(); } } @@ -686,6 +817,82 @@ static void autocontrol_shadow_level6(void) move_1_forward(); } +/* autocontrol_shadow_level12 (seg002:1184) — единственная тень, с которой + * можно и драться, и слиться. Четыре режима подряд, порядок важен. + * + * 1. «Ждать Кида» (Char.room 15, pop_shadow_init == 0). Пока Кид не дошёл + * до x >= 150, тень КАЖДЫЙ КАДР роняется заново — то есть висит в первом + * кадре падения у потолка. Дошёл — флаг поднят, падение пущено. Смысл: + * Кид обязан УВИДЕТЬ, как она сваливается сверху, а не застать готовую. + * 2. У тени вынут меч → обычный боевой ИИ стража. Исключение: если Кид + * намеренно убрал клинок (offguard) и тень ещё не отдышалась после + * попадания (guard_refrac) — она тоже убирает меч (move_4_down). + * 3. У тени меч в ножнах, но Кид ПРИ мече (или не убирал его намеренно): + * близко и видно (< 90) — выхватить клинок из стойки (кадр 15); далеко + * или не видно — стоять, а если Кид зашёл за спину, попятиться. + * 4. Оба без мечей — СЛИЯНИЕ: сойтись ближе 10 и объединиться. + * + * xdiff инициализируется заведомо большим числом (как в SDLPoP): при + * can_guard_see_kid < 2 расстояние не считается вовсе, и ветка «попятиться» + * не должна срабатывать на мусоре. */ +#define FRAME_15_STAND 15 +#define FRAME_3_START_RUN 3 +#define FRAME_127_STEPPING_7 127 +#define FRAME_133_AFTER_STEP 133 +#define SHADOW_WAIT_KID_X 150 +#define SHADOW_SWORD_DIST 90 + +static void autocontrol_shadow_level12(void) +{ + int16_t distance; + + if (Char.room == SHADOW_FIGHT_ROOM && pop_shadow_init == 0) { + if (Opp.x >= SHADOW_WAIT_KID_X) { + do_init_shad(init_shad_12, Char.room, SEQ_7_FALL); + return; + } + pop_shadow_init = 1; + } + if (Char.sword >= SWORD_2_DRAWN) { + if (offguard == 0 || guard_refrac == 0) autocontrol_guard_active(); + else move_4_down(); + return; + } + if (Opp.sword >= SWORD_2_DRAWN || offguard == 0) { + distance = 0x7000; + if (can_guard_see_kid < 2 || + (distance = pop_char_opp_dist()) >= SHADOW_SWORD_DIST) { + if (distance < 0) move_2_backward(); + return; + } + if (Char.frame == FRAME_15_STAND) move_down_forw(); + return; + } + if (pop_char_opp_dist() < SHADOW_UNITE_DIST) { + /* Слияние. Кид ПЕРЕЕЗЖАЕТ на место тени: оригинал переписывает + * charid активного персонажа (а это тень) в Кида и savekid'ит его + * целиком — с позицией, кадром и последовательностью тени. Отсюда + * и «дёрганый» шаг в оригинале: Кид оказывается там, где стояла + * тень. Слот соперника после этого гасится clear_char. */ + pop_flash_color = POP_FLASH_WHITE; + pop_flash_time = 18; + pop_add_life(); /* +1 к потолку HP */ + pop_united_shadow = SHADOW_UNITE_TIME; + Char.charid = CHARID_0_KID; + pop_savekid(); + clear_char(); + return; + } + if (can_guard_see_kid == 2) { + /* Кид бежит к тени — тень бежит навстречу (кадры бега 3..14 и + * шага 127..132). */ + uint8_t of = Opp.frame; + if ((of >= FRAME_3_START_RUN && of < FRAME_15_STAND) || + (of >= FRAME_127_STEPPING_7 && of < FRAME_133_AFTER_STEP)) + move_1_forward(); + } +} + /* autocontrol_mouse (seg002:07EB). Весь «ИИ» мыши — два порога по X: * добежала влево до POP_MOUSE_TURN_X → seq_107 (встать, поднять голову, * развернуться и уйти), вернулась вправо до POP_MOUSE_LEAVE_X → исчезнуть. @@ -706,13 +913,13 @@ static void autocontrol_mouse(void) } } -/* autocontrol_shadow (seg002:081D) — по уровням. Ветка 12 появится со своим - * уровнем. */ +/* autocontrol_shadow (seg002:081D) — по уровням. */ static void autocontrol_shadow(void) { if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4(); if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5(); if (pop_current_level == SHADOW_STEP_LEVEL) autocontrol_shadow_level6(); + if (pop_current_level == SHADOW_FIGHT_LEVEL) autocontrol_shadow_level12(); } /* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */ @@ -857,8 +1064,7 @@ static void play_guard(void) if (Char.charid == CHARID_24_MOUSE) { autocontrol_opponent(); return; } if (Char.alive < 0 && guardhp_curr == 0) { Char.alive = 0; - /* on_guard_killed (seg006:1927) — у нас пока только звук победы, - * которого нет; для уровня 1 других эффектов там нет. */ + on_guard_killed(); /* уровень 13: вспышка + «выход открыт» */ } autocontrol_opponent(); pop_control(); diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index a7939ed..6129138 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -464,6 +464,29 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) pop_env_b(DOOR_FRAM_SLICE[gate_frame], x, yb); } +/* Та же решётка, но в ПЕРЕДНИЙ слой — поверх персонажа, стоящего в проёме + * (draw_gate_fore, seg008:1153). От back-варианта отличается ровно тем, что + * есть в оригинале: только ПРОЗРАЧНЫЕ куски (51 низ + 52 восьмипиксельные + * ломти), без непрозрачного 50 и без хвостового DOOR_FRAM_SLICE — фон под + * баррами уже нарисован, здесь возвращаются только сами прутья. + * Клип по прямоугольнику персонажа делает pop_t_fclip (см. pop_tile.h). */ +static void draw_gate_fore(uint8_t modl, int xh, int dby, int dmy) +{ + int x = xh * 8; + int gate_top_y = dby - 62; + int openness = (((modl > 188) ? 188 : modl) >> 2) + 1; + int gate_bot_y = dmy - openness; + int yb; + + pop_env_b(51, x, gate_bot_y - 2); + yb = gate_bot_y - 12; + if (yb < 192) + while (yb >= 0 && yb > 7 && (yb - 7) > gate_top_y) { + pop_env_b(52, x, yb); + yb -= 8; + } +} + @@ -599,7 +622,17 @@ static void ceil_over_kid_tile(int col) if (code == 20) { /* draw_tile_bottom: стена */ if (!pop_palace) /* seg008:576 */ pop_wall_b(POP_WALL_FRAM_BOTTOM[pop_wall_modifier(-1, col)], x, dby); - wall_pattern(-1, col, 0); + /* wall_pattern ЗДЕСЬ НЕТ — и это не упущение. draw_tile_bottom + * (seg008:588) кладёт в foretable ТОЛЬКО кадр грани `id`, под своим + * `arg_0`; узор вызывается строкой ниже, вне этой ветки, и уходит в + * текущую таблицу, то есть в ФОН. Мы же звали его прямо в переднем + * проходе — узор задней стены штамповался поверх всего, что уже + * нарисовано, включая падающие плиты (найдено прогоном 2026-08-13). + * В фон он попадает там, где и положено, — при отрисовке тайла. + * + * Не путать с fore_only_tile: там узор в переднем слое ЗАКОННЫЙ, + * это порт draw_tile_fore (seg008:715), где он стоит явно рядом с + * главной гранью стены, обязанной перекрывать персонажа. */ } else if (t->bottom_id) { pop_env_b(t->bottom_id, x, dby); } @@ -948,9 +981,27 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y, /* на 141 — до него Kid ещё */ /* в ряду 1 ПОД полом [0,3]) */ if (do_ov) { + /* ВЕСЬ диапазон рядов, а не только два крайних. В оригинале + * (redraw_at_char2, seg003:0645) стоит ровно два вызова — + * char_bottom_row и char_top_row, — и этого хватает, потому что + * там они ВСЕГДА соседние. У нас между ними бывает пропуск: + * char_footprint форсит rT = rB−1 (голова торчит в ряд выше), а + * потом окно fore-клипа расширяет rB ещё на ряд вниз — rT=0, + * rB=2 давали обход рядов 0 и 2 МИМО ряда 1. + * + * Ровно так терялась передняя грань пола у Кида, спрыгнувшего с + * кромки: кадр 103, action 3, ряд по y = 1, а обход шёл по 0 и 2 + * (трасса MAME 2026-08-13: rT=0 rB=2 cL=0 cR=1, ни одного вызова + * overlay_mid_tile). Глубже в падении y_to_row давал уже 2, ряд + * 1 попадал в пару крайних — и плита вдруг начинала рисоваться + * правильно, что и было видно глазом (JUMP-FLOOR-LEGS). */ for (c = cR; c >= cL; c--) { - if (action != 2) other_overlay_tile(rB, c); /* опорный ряд */ - if (rT != rB) other_overlay_tile(rT, c); /* верхний ряд */ + int8_t r2; + for (r2 = rB; r2 >= rT; r2--) { + /* seg003:0645: при hang_climb опорный ряд НЕ трогаем. */ + if (r2 == rB && action == 2) continue; + other_overlay_tile(r2, c); + } } } } @@ -961,6 +1012,23 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y, for (r = rT; r <= rB; r++) for (c = cL; c <= cR; c++) fore_tile(r, c); + /* Первое условие draw_tile_fore (seg008:0D15): персонаж СТОИТ на тайле + * ворот — их решётка возвращается поверх него. Решение приняла карта + * (pop_gate_over_char, разбор там же); здесь остаётся отрисовка. Бары + * рисуются в колонке curr_col+1: графика ворот принадлежит ПРАВОМУ краю + * своего тайла и в оригинале кладётся в проходе следующей колонки. + * + * Один вызов через трамплин на персонажа за кадр — не в тайловом цикле: + * тайл-кандидат всегда ровно один, а fore-проход и так самый горячий + * кусок кадра (memory pop_fore_layer_cost). */ + if (pop_gate_over_char(ch)) { + int8_t gr = ch->curr_row, gc = (int8_t)(ch->curr_col + 1); + if (tile_in_fclip(gr, gc)) { + int dby = 63 * gr + 65; + draw_gate_fore(pop_t_bg ? pop_t_bg[gr * 10 + ch->curr_col] : 0, + POP_COL_XH[gc], dby, dby - 3); + } + } /* redraw_needed_above (seg008:0221): полоса кладки У ПОТОЛКА (ряд −1 = * ряд 2 комнаты сверху). В оригинале она рисуется в самом конце обхода * тайлов — ПОСЛЕ объектов, т.е. поверх персонажа (redraw_frames_above, diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index cee34b0..7531b29 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -160,6 +160,11 @@ void pop_gate_redraw(int row, int col) __banked; * (row,col) и покадровое продвижение+отрисовка на back-странице. tick * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ void pop_loose_mob_spawn(int row, int col) __banked; +/* То же, но в ЯВНО заданной комнате: плита, расшатанная Кидом и брошенная + * (он ушёл в соседнюю комнату), доваливается сама — см. animate_loose в + * pop_trob.c. Дальше кусок летит обычным путём: mob_tick_one умеет чужую + * комнату (mob_tile_at читает уровень, посадку сигналит pop_loose_exit). */ +void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked; void pop_loose_mob_tick(void) __banked; void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */ @@ -169,6 +174,9 @@ void pop_loose_mob_room_changed(void) __banked; /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs, * seg008:1561). Звать после отрисовки персонажей, перед fore-слоем. */ +/* Отрисовка падающих кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed() + * — фон кадра обязан быть готов до любого спрайта. */ +void pop_loose_mob_draw(void) __banked; void pop_loose_mob_draw_over(void) __banked; /* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */ diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 8ce473e..2dcc269 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -83,11 +83,17 @@ int pop_guard_load(void) __banked "SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl", "SKEL\\g3.atl", 0 }; + /* Джафар (уровень 13, tbl_guard_type == 3) — VIZIER.DAT: кадров столько + * же, сколько у стража, страниц пять. */ + static const char *const vizier_names[GUARD_PAGES] = { + "VIZIER\\g0.atl", "VIZIER\\g1.atl", "VIZIER\\g2.atl", + "VIZIER\\g3.atl", "VIZIER\\g4.atl" + }; const char *const *names; int8_t type = pop_level_guard_type(pop_current_level); uint8_t i; if (type < 0) return 0; /* соперников на уровне нет */ - names = (type == 2) ? skel_names : guard_names; + names = (type == 2) ? skel_names : (type == 3) ? vizier_names : guard_names; for (i = 0; i < GUARD_PAGES; i++) { if (names[i] == 0) break; if (atlas_load(&gp[i], names[i]) != 0) return -1; @@ -96,9 +102,10 @@ int pop_guard_load(void) __banked /* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется * из res10.bin по guards_color — это делает pop_guard_set_palette при * входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ - if (type == 2) { + if (type == 2 || type == 3) { + const uint8_t *src = (type == 2) ? pop_skel_pal : pop_vizier_pal; uint8_t buf[64], k; - for (k = 0; k < 64; k++) buf[k] = pop_skel_pal[k]; /* BIOS: буфер в W2 */ + for (k = 0; k < 64; k++) buf[k] = src[k]; /* BIOS: буфер в W2 */ gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); } diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index e94dbc8..d45898a 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -24,6 +24,9 @@ int8_t is_guard_notice; uint8_t guard_skill; int8_t guard_refrac, justblocked, kid_sword_strike; int8_t offguard, holding_sword; +int8_t pop_united_shadow; /* см. pop_guard.h: 0 / счётчик / −1 */ +uint8_t pop_shadow_init; +int8_t pop_guard_notice_timer; /* фора Джафара (уровень 13) */ /* Сид бросков боёвки. В оригинале генератор один на всё, у нас * последовательности разведены по доменам (кладка / анимация тайлов / бой), @@ -62,11 +65,20 @@ void pop_guard_kill(void) { if (Guard.charid == CHARID_4_SKELETON) return; if (!pop_guard_present()) return; - /* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит - * pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше - * общий control() ставит ему seq_71_dying. */ + /* HP не обнуляем — ставим дельту, её применит pop_do_delta_hp. Дальше + * play_guard увидит «жив, но HP ноль», сам переведёт в alive = 0 и + * вызовет on_guard_killed: брызги, вспышка, а на 13-м уровне — флаг + * «выход разрешён». + * + * ОСОЗНАННОЕ РАСХОЖДЕНИЕ (решение пользователя 2026-08-13, ради скорости + * прогонов): оригинал (seg000:786) ставит здесь ещё и `Guard.alive = 0`, + * а гейт в play_guard (seg006:1490) требует `alive < 0` — то есть у него + * чит убивает стража В ОБХОД события смерти, и победа над Джафаром по + * читу выхода на 14-й уровень НЕ открывает. Для отладки это дорого: + * каждый прогон приходится честно выигрывать бой. Убрав одну строку, + * получаем ровно «как будто убил Кид». На игру не влияет — читы в + * релизной сборке выключены (pop_cheats). */ guardhp_delta = (int8_t)-guardhp_curr; - Guard.alive = 0; } /* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */ @@ -83,11 +95,34 @@ uint8_t pop_take_hp(uint8_t count) return 0; } +/* timers (seg003:0735) — кадровые счётчики спецсобытий. + * + * Вспышка слияния с тенью досчитывает до нуля и УХОДИТ В −1: это «уже + * слились», состояние до конца уровня. Именно на −1 смотрят check_shadow + * (больше не поднимать) и «появление плит» в комнатах 2/13 (pop_map). + * Ноль как конечное значение не годится — он неотличим от «не было». */ +void pop_frame_timers(void) +{ + if (pop_united_shadow > 0) { + --pop_united_shadow; + if (pop_united_shadow == 0) pop_united_shadow = -1; + } + if (pop_guard_notice_timer > 0) --pop_guard_notice_timer; +} + /* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить * её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */ void pop_do_delta_hp(void) { int16_t hp; + /* Спецсобытие уровня 12 (seg000:1518): ранил ТЕНЬ — ранил себя. Урон + * не делится, а КОПИРУЕТСЯ: тень теряет 1 HP и Кид теряет 1 HP. Без + * этого бой с тенью теряет смысл — её можно было бы просто зарубить. + * Проверка идёт по слоту соперника (у оригинала — по Opp, но в момент + * do_delta_hp там всегда пара Кид/соперник). */ + if (pop_current_level == SHADOW_FIGHT_LEVEL && + Guard.charid == CHARID_1_SHADOW && guardhp_delta != 0) + hitp_delta = guardhp_delta; if (hitp_delta) { hp = (int16_t)hitp_curr + hitp_delta; if (hp < 0) hp = 0; diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index af9d118..c4fa54f 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -66,6 +66,9 @@ #define CHARID_0_KID 0 #define CHARID_1_SHADOW 1 #define CHARID_2_GUARD 2 +/* charid_3 не используется: Джафар (уровень 13) — обычный charid_2_guard, + * своим charid оригинал помечает ТОЛЬКО скелета (enter_guard, seg002:196). + * Отличают Джафара набор спрайтов (VIZIER.DAT), skill 9 и три события. */ #define CHARID_4_SKELETON 4 #define CHARID_5_PRINCESS 5 /* МЫШЬ (спецсобытие уровня 8) — тоже персонаж слота Guard: своей таблицы @@ -129,8 +132,41 @@ /* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех * остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см. - * pop_check_can_guard_see_kid. */ + * pop_check_can_guard_see_kid. + * + * Сцена целиком (seg002:0070 + seg002:1184): в комнате 15 лежит МЕЧ (0,1); + * пока он на месте, тень не появляется. Кид его берёт — тень падает в ту + * же комнату, и дальше либо бой (у обоих клинки), либо, если Кид уберёт + * меч, — слияние. Комната 18 при уходе ВПРАВО «съедает» меч с карты + * (sword_disappears), чтобы вернуться и подобрать его второй раз было + * нельзя. Проверено по res2012.bin, ../docs/levels_12_15_plan.md §7.3. */ #define SHADOW_FIGHT_LEVEL 12 +#define SHADOW_FIGHT_ROOM 15 +#define SHADOW_SWORD_TILEPOS 1 /* (кол 1, ряд 0) — меч уровня 12 */ +#define SWORD_GONE_ROOM 18 /* уход ВПРАВО отсюда убирает меч */ +/* Слияние (autocontrol_shadow_level12): дистанция, вспышка, её длительность. */ +#define SHADOW_UNITE_DIST 10 +#define SHADOW_UNITE_TIME 42 + +/* Уровень 13 — ДЖАФАР (custom->jaffar_*, дефолты SDLPoP). Своего ИИ у него + * нет вовсе: `autocontrol_Jaffar` (seg002:085A) — это `autocontrol_guard`, + * то есть обычный бой стражи. Отличают его только данные (VIZIER.DAT, + * skill 9, 6 HP — всё уже в pop_level_cold.c) и три события ниже. + * + * Встреча: Кид уходит ВПРАВО из комнаты 3 — а `комната 3.right = 1`, где + * Джафар и стоит (проверено по res2013.bin, ../docs/levels_12_15_plan.md + * §7.2). Ему даётся фора JAFFAR_NOTICE_TIME кадров: пока таймер идёт, он не + * встаёт в боевую стойку, и это единственная пауза во всей сцене. + * + * Выход: смерть Джафара сама дверь НЕ открывает — она ставит + * pop_leveldoor_open = 2, а дверь открывает КНОПКА комнаты 24 (tilepos 0, + * реальный opener), «нажимаемая» при уходе Кида ВЛЕВО. */ +#define JAFFAR_LEVEL 13 +#define JAFFAR_MEET_ROOM 3 /* уход ВПРАВО отсюда будит Джафара */ +#define JAFFAR_NOTICE_TIME 28 /* ≈2,3 с форы (28/12) */ +#define JAFFAR_EXIT_ROOM 24 +#define JAFFAR_EXIT_TILEPOS 0 +#define JAFFAR_FLASH_TIME 18 /* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */ #define NUM_GUARD_SKILLS 12 @@ -184,6 +220,23 @@ extern int8_t guard_refrac, justblocked, kid_sword_strike; * holding_sword — клинок в руке (для индикатора HP стража). */ extern int8_t offguard, holding_sword; +/* Спецсобытие уровня 12 — СЛИЯНИЕ С ТЕНЬЮ (united_with_shadow, seg000:288). + * Значение читается как три разных состояния, и это НЕ флаг: + * 0 — тень ещё отдельный персонаж (или уровень не 12); + * >0 — идёт вспышка слияния, счётчик кадров (42 → 1), убывает в + * pop_shadow_timer; пока он идёт, Кид мигает спрайтами тени; + * −1 — «уже слились», навсегда до конца уровня. Именно по нему + * check_shadow больше не поднимает тень, а в комнатах 2/13 + * появляются недостающие плиты пола. + * Переход 0 → −1 идёт ЧЕРЕЗ счётчик: таймер, досчитав до нуля, уводит его + * ещё на единицу вниз (seg003:503) — отсюда и знаковый тип. + * + * pop_shadow_init (shadow_initialized, seg000:309) — «тень уже дождалась + * Кида в комнате 15»: до этого autocontrol_shadow_level12 роняет её заново + * каждый кадр, чтобы Кид застал падение, а не готовую стойку. */ +extern int8_t pop_united_shadow; +extern uint8_t pop_shadow_init; + /* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */ extern pop_rnd_t pop_fight_seed; @@ -215,6 +268,33 @@ void pop_check_mouse(void) __banked; * возвращается и во втором случае тоже. */ uint8_t pop_check_shadow(uint8_t room) __banked; +/* sword_disappears (seg002:0536) — спецсобытие уровня 12: уход ВПРАВО из + * комнаты 18 убирает меч из комнаты 15. Звать при уходе вбок ДО смены + * комнаты, рядом с чекпойнтом уровня 3. */ +void pop_sword_disappears(void) __banked; + +/* meet_Jaffar (seg002:0544) — уход ВПРАВО из комнаты 3 уровня 13 даёт + * Джафару фору; звать рядом с pop_sword_disappears (та же ветка leave_room). + * Jaffar_exit (seg002:0517) — уход ВЛЕВО при pop_leveldoor_open == 2 жмёт + * кнопку комнаты 24 и открывает дверь уровня. */ +void pop_meet_jaffar(void) __banked; +void pop_jaffar_exit(void) __banked; + +/* check_killed_shadow (seg006:2033) — спецсобытие уровня 12: смерть тени + * убивает Кида и наоборот. Звать раз в кадр ДО тиков персонажей, рядом с + * pop_check_skel; на прочих уровнях выходит первой строкой. */ +void pop_check_killed_shadow(void) __banked; + +/* timers (seg003:0735) — кадровые счётчики спецсобытий: вспышка слияния с + * тенью и фора Джафара. Звать раз в кадр из главного цикла. Резидент: два + * вычитания, банк ради них не разворачиваем. */ +void pop_frame_timers(void); + +/* guard_notice_timer (seg000:307) — фора Джафара после встречи: пока >0, он + * не встаёт в боевую стойку. Ставит pop_meet_jaffar, убавляет + * pop_frame_timers, читает autocontrol_guard_inactive. */ +extern int8_t pop_guard_notice_timer; + /* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в * кадре как в play_frame: hurting -> hurt -> do_delta_hp. */ void pop_check_sword_hurting(void) __banked; diff --git a/applications/PoP/roomtest/pop_guard_cold.c b/applications/PoP/roomtest/pop_guard_cold.c index 7a82446..87f5e22 100644 --- a/applications/PoP/roomtest/pop_guard_cold.c +++ b/applications/PoP/roomtest/pop_guard_cold.c @@ -102,8 +102,15 @@ void pop_guard_enter(uint8_t room) if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return; /* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты * палитры общие на экран, и смена посреди кадра дала бы стража в новой - * палитре с полосой HP в старой. */ - pop_guard_set_palette(color); + * палитре с полосой HP в старой. + * + * Цвет из данных комнаты берётся ТОЛЬКО у обычного стража (enter_guard, + * seg002:183: `curr_guard_color = 0`, если tbl_guard_type != 0). У + * скелета и Джафара палитра своя, из файла набора, и подменять её + * нельзя — иначе визирь на уровне 13 покрасился бы в цвет стража, + * записанный в его комнате. */ + pop_guard_set_palette(pop_level_guard_type(pop_current_level) == 0 + ? color : 0); /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из * колонки тайла, а не берётся из файла. */ Guard.room = room; diff --git a/applications/PoP/roomtest/pop_guard_pal.h b/applications/PoP/roomtest/pop_guard_pal.h index faeba24..8e394f4 100644 --- a/applications/PoP/roomtest/pop_guard_pal.h +++ b/applications/PoP/roomtest/pop_guard_pal.h @@ -28,4 +28,9 @@ static const uint8_t pop_skel_pal[64] = { 0x00,0x00,0x00,0x00,0xFF,0xFF,0x55,0x00,0x89,0x79,0xBA,0x00,0xF2,0xFF,0xFF,0x00,0x00,0x00,0xAA,0x00,0xAA,0x00,0xAA,0x00,0x00,0x55,0xAA,0x00,0xDA,0xE6,0xE6,0x00,0x55,0x55,0x55,0x00,0xFF,0x55,0x55,0x00,0x55,0xFF,0x55,0x00,0xFF,0xFF,0x55,0x00,0x55,0x55,0xFF,0x00,0x00,0x00,0x00,0x00,0x55,0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0x00, }; +/* Джафар (VIZIER/res750.pal): свой набор — свой цвет. */ +static const uint8_t pop_vizier_pal[64] = { + 0x00,0x00,0x00,0x00,0x79,0x69,0xFF,0x00,0x7D,0x4C,0xBE,0x00,0xFF,0xFF,0xFF,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0xD2,0xB6,0xD2,0x00,0x91,0x7D,0x91,0x00,0xFF,0xDA,0xFF,0x00,0x4C,0x81,0xA1,0x00,0x71,0xBA,0xEA,0x00,0xEA,0x44,0xB2,0x00,0xEA,0x44,0xB2,0x00,0x55,0xFF,0xFF,0x00,0x99,0x2C,0x75,0x00, +}; + #endif diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index df17218..6568637 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -236,6 +236,16 @@ void pop_kid_hp_reset(void) __banked pop_kid_dead = 0; } +/* Бесшовный переход (seamless, seg003:158): уровень стартует, НЕ трогая HP — + * Кид уносит на следующий и потолок, и полученный урон, потому что для него + * это не «новый уровень», а продолжение того же бега. Всё остальное, что + * делает pop_kid_hp_reset, нужно и здесь: дельта кадра и признак смерти. */ +void pop_kid_hp_carry(void) __banked +{ + hitp_delta = 0; + pop_kid_dead = 0; +} + /* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */ void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; } @@ -249,25 +259,39 @@ static void pop_kid_add_life(void) hitp_delta = (int8_t)(hitp_max - hitp_curr); } +/* Тот же add_life наружу: слияние с тенью (уровень 12) даёт ровно ту же + * прибавку, что большая склянка, а живёт ИИ тени в другом банке. */ +void pop_add_life(void) __banked { pop_kid_add_life(); } + /* Урон — через общий pop_take_hp (резидент pop_guard.c): физика работает * над Char, значит и HP обязаны убавляться у ТОГО, кто сейчас в Char. Своя * копия take_hp здесь была Kid-only и правила hitp_curr мимо дельты. */ -void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked +/* Перечитать fg соседей из уровня, НЕ трогая историю коллизий. Отдельно от + * pop_map_set_edges, потому что нужен и посреди жизни в комнате: тайл соседа + * может измениться, пока тот не отрисован (плита, брошенная Кидом на + * полпути, доваливается заочно — animate_loose в pop_trob.c). g_lcol/g_rcol + * до этого были снимком НА МОМЕНТ ВХОДА, и коллизия у шва продолжала считать + * провалившуюся плиту полом. */ +void pop_map_refresh_edges(void) __banked { uint8_t i; + if (!g_link_l && !g_link_r) return; + pop_level_access_begin(); + for (i = 0; i < 30; i++) { + if (g_link_l) g_lcol[i] = pop_level_tile_raw(g_link_l, i); + if (g_link_r) g_rcol[i] = pop_level_tile_raw(g_link_r, i); + } + pop_level_access_end(); +} + +void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked +{ g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down; /* fg соседей — прямо из уровня, одной скобкой доступа на обе комнаты. * Держать ещё одну копию в приложении незачем: карта — единственный её * потребитель (отрисовке швов хватает кромочных колонок pop_room_load). */ - if (left || right) { - pop_level_access_begin(); - for (i = 0; i < 30; i++) { - if (left) g_lcol[i] = pop_level_tile_raw(left, i); - if (right) g_rcol[i] = pop_level_tile_raw(right, i); - } - pop_level_access_end(); - } + pop_map_refresh_edges(); /* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия * недействительны (в оригинале prev_coll_room != curr_row_coll_room и * бампа на кадре входа нет). БОКОВОЙ переход это отменяет: вызывающий @@ -1349,13 +1373,52 @@ uint8_t pop_get_item_action(void) __banked } } +/* Спецсобытие уровня 12 «появление плит» (seg006:1063, ветка внутри + * check_on_floor). После слияния с тенью (pop_united_shadow == −1) в + * верхнем ряду комнат 2 и 13 пропасти становятся проходимыми: персонаж, + * который ДОЛЖЕН БЫЛ провалиться, вместо этого достраивает под собой пол. + * Плита появляется ровно под ним, шаг за шагом — это не «включили этаж», а + * дорожка, возникающая под ногами. + * + * Возврат 1 = «пол появился, падать не надо». Перерисовываем сам тайл и + * соседа справа на ОБЕИХ страницах — тем же приёмом, что pop_skel_wake_tile + * (у оригинала это пара set_redraw_full/set_wipe на curr_tilepos и +1). + * + * Комната берётся РЕЗОЛВНУТОЙ (tile_room_of): персонаж может стоять в шве, + * и тогда пол обязан появиться в соседней комнате, как и у оригинала — + * там get_tile переставляет curr_room/curr_tilepos до присваивания. */ +#define HIDDEN_FLOOR_ROOM_A 2 +#define HIDDEN_FLOOR_ROOM_B 13 +#define HIDDEN_FLOOR_COL_B 6 /* в комнате 13 — только правая половина */ + +static uint8_t floors_appear(void) +{ + uint8_t room, tp; + if (pop_current_level != SHADOW_FIGHT_LEVEL) return 0; + if (pop_united_shadow >= 0) return 0; /* только ПОСЛЕ слияния */ + if (Char.curr_row != 0) return 0; + if (Char.room != HIDDEN_FLOOR_ROOM_A && + !(Char.room == HIDDEN_FLOOR_ROOM_B && Char.curr_col >= HIDDEN_FLOOR_COL_B)) + return 0; + room = tile_room_of(Char.curr_col, Char.curr_row, &tp); + if (!room) return 0; + pop_level_set_tile(room, tp, TILE_FLOOR); + if (room == g_room) { + g_fg[tp] = TILE_FLOOR; + pop_set_redraw(tp, POP_RD_FLOOR, 2); + if ((tp % 10) < 9) + pop_set_redraw((uint8_t)(tp + 1), POP_RD_FLOOR, 2); + } + return 1; +} + /* ---- Диспетчер ----------------------------------------------------- */ static void check_on_floor(void) { if (kid_cur_flags() & FRAME_NEEDS_FLOOR) { if (get_tile_at_char() == TILE_WALL) in_wall(); /* забежал в стену -> выровнять */ - if (!tile_is_floor(g_curr_tile)) + if (!tile_is_floor(g_curr_tile) && !floors_appear()) start_fall(); } } @@ -1466,6 +1529,40 @@ static void bumped_floor(void) is_guard_notice = 1; /* bumped_sound (seg004:05F1) */ } +/* bump_into_opponent (seg003:08AA). Зовётся из play_kid_frame (seg000:1238) + * между set_char_collision и check_collisions, и ТОЛЬКО у Кида — у стража + * этой ветки нет (см. комментарий к guard_phys ниже). + * + * ЗАЧЕМ. Это единственная защита оригинала от «вошёл в комнату — сразу + * труп». Стража при входе никуда не отбрасывает: enter_guard (seg002:0112) + * ставит его ровно в его тайл из данных уровня, и Кид вполне может войти + * вплотную. Спасает то, что БЕЗОРУЖНЫЙ Кид, столкнувшись с вооружённым + * соперником лицом к лицу, не получает укол, а ОТСКАКИВАЕТ (seq_47). + * Остальные три страховки у нас уже есть: Кид сам достаёт меч + * (control_standing, pop_ctrl.c), страж поднимается в стойке покоя с мечом + * в ножнах (pop_guard_cold.c) и не колет ближе 12 (guards.c). + * + * Расхождений с оригиналом нет. Опциональные фиксы SDLPoP + * (fix_painless_fall_on_guard — падение на стража с высоты перестаёт быть + * безболезненным; fix_jumping_over_guard — нельзя перепрыгнуть) намеренно НЕ + * портированы: базовое поведение движка 1989 года. */ +static void bump_into_opponent(void) +{ + int16_t distance; + if (can_guard_see_kid < 2) return; + if (Char.sword != 0) return; /* Кид уже в боевой стойке */ + if (Opp.sword == 0) return; /* у соперника меч в ножнах */ + if (Opp.action >= 2) return; /* только stand / run_jump */ + if (Char.direction == Opp.direction) return; /* должны быть лицом к лицу */ + distance = pop_char_opp_dist(); + if (distance < 0) distance = -distance; + if (distance > 15) return; + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + Char.fall_y = 0; + pop_char_set_seq(SEQ_47_BUMP); + play_seq(); +} + /* ---- check_collisions (seg004:0004) -------------------------------- * * ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка * удара выбирается обходом 9→0 по переходу флага 0→1. @@ -2112,6 +2209,12 @@ static void check_press(void) static void do_knock(int tile_row) { int col, pos; + /* СПЕЦКЕЙС УРОВНЯ 13 (loose_make_shake, seg007:949): сотрясение НЕ + * трясёт плиты вовсе. Причина та же, что у первого спецкейса: тряска + * ставит фазу 0x80, а на этом уровне у плит уже лежит СВОЙ отложенный + * старт (0xF0..0xFF), и приземление Кида сбросило бы его на 0x80 — + * гряда посыпалась бы разом вместо вразнобой. */ + if (pop_current_level == POP_LOOSE_TILES_LEVEL) return; if (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */ if (!g_above || !g_link_u) return; for (col = 0; col < 10; col++) @@ -2139,18 +2242,120 @@ void pop_check_knock(void) __banked } } +/* Сдать НЕЗАКОНЧЕННЫЕ loose уходящей комнаты в персистентное хранилище — + * звать ПЕРЕД сменой комнаты, пока g_room/g_fg/g_link_u ещё старые. + * + * pop_loose_modif[30] — фаза тайлов ТЕКУЩЕЙ комнаты, и раньше смена комнаты + * её просто теряла: плита, по которой Кид пробежал и ушёл вправо, оставалась + * целой (уровень 10, комната 2, тайл 2,9). В оригинале терять нечего — + * там loose это trob в общем списке, а фаза лежит в curr_room_modif, то есть + * в самой таблице уровня. Порт того же: фаза уезжает в room_modif (он + * персистентен per-room), а досчитывает её animate_loose в pop_trob.c. + * + * Пишем фазу ВСЕМ loose-тайлам, включая нулевые: room_modif — источник + * истины, а pop_loose_modif лишь его копия на время пребывания в комнате, + * и оставить там прошлое значение значило бы «оживить» уже отдрожавшую + * плиту при следующем входе. trob заводим только на незаконченные. */ +void pop_loose_leave_room(void) __banked +{ + uint8_t *mod; + int i; + if (!g_fg || !g_room) return; + mod = pop_trob_modif(g_room); + if (mod) { + for (i = 0; i < 30; i++) { + if ((g_fg[i] & 0x1F) != TILE_LOOSE) continue; + mod[i] = pop_loose_modif[i]; + if (pop_loose_modif[i]) pop_add_trob(g_room, (uint8_t)i, 0); + } + } + /* Плиты-ПОТОЛКИ — ряд 2 комнаты сверху, tilepos 20+col. */ + if (g_above && g_link_u) { + mod = pop_trob_modif(g_link_u); + if (mod) { + for (i = 0; i < 10; i++) { + if ((g_above[i] & 0x1F) != TILE_LOOSE) continue; + mod[20 + i] = pop_ceil_modif[i]; + if (pop_ceil_modif[i]) + pop_add_trob(g_link_u, (uint8_t)(20 + i), 0); + } + } + } +} + +/* Решётка ворот должна идти ПОВЕРХ персонажа? Порт первого условия + * draw_tile_fore (seg008:0D15): + * + * if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row && + * Kid.curr_col == drawn_col - 1 && Kid.room != room_R) + * draw_gate_fore(); + * + * То есть: рисуя тайл (row, col), оригинал кладёт бары ворот в FOREtable — + * поверх всего, что уже нарисовано, — если тайл СЛЕВА это ворота и персонаж + * стоит ИМЕННО НА НИХ. Графика ворот принадлежит правому краю своего тайла + * и рисуется в проходе СЛЕДУЮЩЕЙ колонки, отсюда сдвиг на единицу. + * + * Живёт в карте, а не в слое фона, по двум причинам: спрашивает она про + * ТАЙЛ и позицию персонажа (это знание карты), и её линкует хост-харнесс — + * решение проверяется тестом без эмулятора (t_char: gate_fore_over_char). + * Слою фона остаётся только рисование (draw_gate_fore в pop_bg.c). + * + * РАСХОЖДЕНИЕ С ОРИГИНАЛОМ (осознанное, см. docs/impl_diff.md): оригинал + * жёстко спрашивает про Kid, потому что foretable у него ОДНА на проход + * тайлов; у нас fore-проход идёт по персонажу, поэтому спрашиваем про того, + * кого рисуем. Наш вариант — надмножество: страж, стоящий в проёме ворот, + * у нас тоже уходит ЗА решётку, а в оригинале остался бы перед ней. */ +uint8_t pop_gate_over_char(const pop_char_t *ch) __banked +{ + if (!ch || ch->room != g_room) return 0; + if (ch->curr_row < 0 || ch->curr_row > 2) return 0; + /* Колонка 9: бары легли бы в колонку 10, то есть в СОСЕДНЮЮ комнату — + * у оригинала это отсекает `Kid.room != room_R`. */ + if (ch->curr_col < 0 || ch->curr_col > 8) return 0; + return (uint8_t)(get_tile(ch->curr_col, ch->curr_row) == TILE_GATE); +} + +/* СТАРТ/РЕСТАРТ уровня: забыть фазы, НЕ сдавая их в room_modif — в + * оригинале всё состояние сносит сам load_level (seg003:57). Без этого + * pop_loose_leave_room у первого же enter_room занесла бы в только что + * восстановленный уровень дрожь из прошлой жизни: pop_trob_reset уже + * отработал, room_modif свежий, а pop_loose_modif — ещё старый. */ +void pop_loose_forget(void) __banked +{ + int i; + for (i = 0; i < 30; i++) pop_loose_modif[i] = 0; + for (i = 0; i < 10; i++) pop_ceil_modif[i] = 0; +} + /* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest * индексируются позицией тайла и не должны утечь из старой комнаты в новую. * Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и - * должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */ + * должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). + * + * Вместо нулей забираем фазу НОВОЙ комнаты из room_modif — обратная + * половина pop_loose_leave_room: расшатанная и брошенная плита, к которой + * Кид вернулся, продолжает дрожать с той же фазы, а не с покоя. trob, + * который крутил её заочно, снимет сам animate_loose (room == cur_room). */ void pop_loose_reset(void) __banked { + uint8_t *mod; int i; + + mod = g_room ? pop_trob_modif(g_room) : 0; for (i = 0; i < 30; i++) { - pop_loose_modif[i] = 0; + pop_loose_modif[i] = (mod && g_fg && (g_fg[i] & 0x1F) == TILE_LOOSE) + ? mod[i] : 0; } + mod = (g_above && g_link_u) ? pop_trob_modif(g_link_u) : 0; for (i = 0; i < 10; i++) { - pop_ceil_modif[i] = 0; + pop_ceil_modif[i] = (mod && (g_above[i] & 0x1F) == TILE_LOOSE) + ? mod[20 + i] : 0; + /* ...и СНЯТЬ заочный trob: плиту-потолок отсюда снова ведёт + * pop_loose_tick. Без этого её крутили бы ДВОЕ — trob по + * room_modif и мы по pop_ceil_modif, — и она проваливалась бы + * дважды (animate_loose снимает сам только trob'ы cur_room, а + * комната сверху — не cur_room). */ + if (pop_ceil_modif[i]) pop_add_trob(g_link_u, (uint8_t)(20 + i), -1); } pop_ceil_fell = 0; pop_debris_at = 0; @@ -2158,6 +2363,82 @@ void pop_loose_reset(void) __banked pop_loose_mob_room_changed(); } +/* remove_loose (seg007:0EB8) для ОДНОГО тайла, наружу — ради честного + * loose_fall: падающий кусок сбивает плиту НЕМЕДЛЕННО, в том же кадре + * (pop_room.c, mob_break_loose). Здесь, а не в pop_room, потому что тут + * живут и рабочая копия тайлов комнаты (g_fg), и фазы плит. + * + * Перерисовываем ДВЕ клетки — саму и соседа справа. Передний торец пола + * заезжает в соседний тайл (правая грань соседа всегда в backtable), и без + * второй пометки от исчезнувшей плиты оставался висеть торец: тайл пустой, + * а его передняя грань нарисована. Тем же приёмом, что pop_skel_wake_tile + * и что set_redraw_full+set_wipe на curr_tilepos и curr_tilepos+1 в + * оригинале. */ +void pop_loose_remove(uint8_t room, uint8_t tilepos) __banked +{ + uint8_t *mod = pop_trob_modif(room); + if (tilepos >= 30) return; + if (room == g_room) { + g_fg[tilepos] = TILE_EMPTY; + pop_loose_modif[tilepos] = 0; + pop_set_redraw(tilepos, POP_RD_LOOSE_GONE, 2); + if ((tilepos % 10) < 9) + pop_set_redraw((uint8_t)(tilepos + 1), POP_RD_FLOOR, 2); + } else { + /* Комната не на экране, но её кромочная колонка видна в шве — те же + * снимки, что обновляет animate_loose. */ + pop_neigh_dirty = 1; + } + /* Модификатор пустой клетки = ТИП УРОВНЯ (по нему draw_tile_right + * выбирает blueline у дыры), тайл в странице уровня — сразу. */ + if (mod) mod[tilepos] = pop_palace; + pop_level_set_tile(room, tilepos, TILE_EMPTY); +} + +/* check_fall_flo (seg000:1317) — СПЕЦСОБЫТИЕ уровня 13: гряда плит над + * комнатой сыплется на Кида сама, вразнобой. + * + * Как это устроено в оригинале: при входе в комнату 23 или 16 шести плитам + * ряда 2 комнаты СВЕРХУ раздаётся ОТРИЦАТЕЛЬНЫЙ модификатор −(rnd & 15). + * Отрицательное значение — это старший бит, то есть для animate_loose оно + * выглядит как «тряска», а на уровне 13 тряска намеренно не гасится + * (спецкейс в pop_loose_tick). Фаза считает вверх, доходит до нуля и + * дальше идёт обычным отсчётом до провала — получается разная задержка у + * каждой плиты. + * + * Плиты ряда 2 верхней комнаты у нас — это pop_ceil_modif (плиты-потолки), + * так что раздаём фазу именно туда. + * + * ОДНО отличие от оригинала, вынужденное представлением: у нас фаза 0 = «не + * анимируется», плита с такой фазой в цикле пропускается вовсе. Значит + * счёт, дойдя до нуля, оборвался бы — поэтому, во-первых, тик перескакивает + * ноль (см. pop_loose_tick), а во-вторых, стартовое значение берётся на + * единицу «отрицательнее» (−n−1): вместе это даёт ровно ту же суммарную + * задержку n + 11 кадров, что и у оригинала. + * + * Звать при входе в комнату, после pop_loose_reset (тот забирает фазы из + * room_modif) — как check_the_end зовёт его после start_chompers. */ +static pop_rnd_t loose_seed; /* свой домен последовательности, см. pop_geom.h */ + +void pop_check_fall_flo(void) __banked +{ + uint8_t col; + if (pop_current_level != POP_LOOSE_TILES_LEVEL) return; + if (g_room != POP_LOOSE_TILES_ROOM1 && g_room != POP_LOOSE_TILES_ROOM2) return; + if (!g_above || !g_link_u) return; + for (col = POP_LOOSE_TILES_FIRST - 20; col <= POP_LOOSE_TILES_LAST - 20; col++) { + uint8_t t = g_above[col]; + /* Те же отсевы, что в make_loose_fall: не loose и «solid» loose + * пропускаются молча (в комнате 1 тайл 24 пустой — диапазон 22..27 + * намеренно надмножество), уже идущий отсчёт не перебиваем. */ + if ((t & 0x1F) != TILE_LOOSE || (t & 0x20)) continue; + if ((int8_t)pop_ceil_modif[col] > 0) continue; + pop_ceil_modif[col] = + (uint8_t)(-(int8_t)(pop_prandom(&loose_seed, 255) & 0x0F) - 1); + pop_set_redraw_above(col, POP_RDA_CEIL, 1); + } +} + /* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге * (кадры 5..14) не бьёт (кроме уровня 13 — его тут нет); в висе/подъёме тоже * (action >= hang_climb, кроме turn). Иначе −1 HP: смерть → seq_22 (crushed), @@ -2169,7 +2450,10 @@ void pop_loose_reset(void) __banked static void fell_on_your_head(void) { uint8_t frame = Char.frame, action = Char.action; - if ((frame < 5 || frame >= 15) && + /* СПЕЦКЕЙС УРОВНЯ 13 (seg007:1218): там плита бьёт и В БЕГЕ. На прочих + * уровнях бегущего она пропускает — иначе гряда падающих плит, сквозь + * которую и надо пробегать, не представляла бы никакой угрозы. */ + if ((pop_current_level == POP_LOOSE_TILES_LEVEL || frame < 5 || frame >= 15) && (action < ACT_HANGCLIMB || action == ACT_TURN)) { Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; if (pop_take_hp(1)) { /* «брызги» даст pop_do_delta_hp */ @@ -2221,8 +2505,22 @@ void pop_loose_tick(void) __banked for (pos = 0; pos < 30; pos++) { if (pop_loose_modif[pos] != 0) { uint8_t m = ++pop_loose_modif[pos]; + /* Отложенный старт уровня 13 досчитал до нуля. Ноль у нас + * означает «не анимируется», и плита застряла бы навсегда — + * перескакиваем на 1. Стартовое значение выбрано с учётом + * этого перескока (см. pop_check_fall_flo). */ + if (m == 0) m = pop_loose_modif[pos] = 1; if (m & 0x80) { /* тряска (do_knock) */ - if (m >= 0x84) { /* конец тряски: сброс + покой */ + /* СПЕЦКЕЙС УРОВНЯ 13 (seg007:823): фазу со старшим битом НЕ + * гасим и не трясём. На нём check_fall_flo раздаёт плитам + * ОТРИЦАТЕЛЬНЫЙ старт (0xF0..0xFF) как отложенный таймер, и + * обычная ветка «тряска кончилась на 0x84» убила бы его в + * первом же кадре — плита не упала бы никогда. Пропущенная + * инкрементом фаза сама дойдёт до 0x00, там старший бит + * снимется, и дальше пойдёт обычный отсчёт до провала. */ + if (pop_current_level == POP_LOOSE_TILES_LEVEL) { + /* ждём молча */ + } else if (m >= 0x84) { /* конец тряски: сброс + покой */ pop_loose_modif[pos] = 0; /* modif уже 0 → кадр покоя обоих тайлов (col и col+1: * правая грань loose живёт в соседе). ОБЕ страницы — @@ -2231,6 +2529,13 @@ void pop_loose_tick(void) __banked } else { pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */ } + } else if (m >= 11 && pop_loose_fell) { + /* Сигнал прошлого провала ещё не разобран главным циклом (он + * ОДИН на кадр) — придержим плиту кадр, откатив фазу к 10. + * Иначе из двух плит, провалившихся в одном кадре, дыру + * запекала бы только вторая, а первая оставалась нарисованной + * целой (уровень 13: check_fall_flo роняет гряду разом). */ + pop_loose_modif[pos] = 10; } else if (m >= 11) { /* loose_floor_delay — падение */ /* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!). * Debris появляется ТОЛЬКО там, где приземлился падающий @@ -2244,6 +2549,11 @@ void pop_loose_tick(void) __banked pop_trob_modif(g_room)[pos] = pop_palace; pop_loose_modif[pos] = 0; pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2); /* запечь пусто, обе стр. */ + /* ...и СОСЕДА СПРАВА: передний торец пола заезжает в его + * клетку, и без этой пометки он оставался висеть над пустым + * тайлом. То же, что делает pop_skel_wake_tile. */ + if ((pos % 10) < 9) + pop_set_redraw((uint8_t)(pos + 1), POP_RD_FLOOR, 2); pop_loose_mob_spawn(row, col); /* отрыв падающего куска */ pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */ } else { /* отсчёт 1..10 — дрожащий кадр */ @@ -2258,10 +2568,19 @@ void pop_loose_tick(void) __banked for (col = 0; col < 10; col++) { if (pop_ceil_modif[col] != 0) { uint8_t m = ++pop_ceil_modif[col]; + if (m == 0) m = pop_ceil_modif[col] = 1; /* см. pop_loose_tick выше */ if (m & 0x80) { /* тряска от сотрясения (do_knock) */ - if (m >= 0x84) { pop_ceil_modif[col] = 0; + /* Тот же спецкейс уровня 13, и здесь он ГЛАВНЫЙ: плиты, + * которым check_fall_flo раздал отложенный старт, лежат + * именно в ряду 2 комнаты СВЕРХУ, то есть ведёт их этот + * цикл, а не соседний. */ + if (pop_current_level == POP_LOOSE_TILES_LEVEL) { + /* ждём молча */ + } else if (m >= 0x84) { pop_ceil_modif[col] = 0; pop_set_redraw_above(col, POP_RDA_CEIL, 2); } else pop_set_redraw_above(col, POP_RDA_CEIL, 1); + } else if (m >= 11 && pop_ceil_fell) { + pop_ceil_modif[col] = 10; /* сигнал занят — кадр подождём */ } else if (m >= 11) { /* loose_floor_delay — плита рушится */ pop_ceil_modif[col] = 0; if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */ @@ -2629,6 +2948,7 @@ static void kid_phys(void) fall_accel(); fall_speed(); determine_col(); + bump_into_opponent(); /* seg003: безоружный Кид отскакивает от стража */ check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */ check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_action(); diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 6d7f8d8..6269868 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -12,6 +12,7 @@ #define POP_MAP_H #include +#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */ /* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10). * col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив @@ -27,6 +28,10 @@ void pop_map_set(uint8_t *fg) __banked; * то есть заведомо выходит за комнату; кромочных колонок не хватало, и * get_tile отдавал мнимую стену (BUG-SWORD-GHOST-1). Звать в enter_room. */ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked; +/* Перечитать fg соседей (кромочные колонки коллизии) из уровня, не сбрасывая + * историю перекрытий. Нужен, когда тайл соседа изменился, пока тот не + * отрисован — см. pop_neigh_dirty в pop_state.h. */ +void pop_map_refresh_edges(void) __banked; /* БОКОВОЙ переход в соседнюю комнату: перенумеровать историю флагов * перекрытия вместо того, чтобы её выбросить (иначе Кид с разбега проходит @@ -64,7 +69,15 @@ extern uint8_t pop_kid_dead; extern uint8_t hitp_curr; extern uint8_t hitp_max; /* стартовое HP (для индикатора) */ extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */ +/* check_fall_flo (seg000:1317) — спецсобытие уровня 13: вход в комнату 23 + * или 16 роняет на Кида гряду плит из комнаты СВЕРХУ, каждую со своей + * задержкой. Звать при входе в комнату ПОСЛЕ pop_loose_reset (порядок + * check_the_end: start_chompers → check_fall_flo → check_shadow). */ +void pop_check_fall_flo(void) __banked; + void pop_kid_hp_reset(void) __banked; +/* То же для БЕСШОВНОГО старта уровня (pop_seamless): HP не трогаем вовсе. */ +void pop_kid_hp_carry(void) __banked; /* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт * уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень @@ -89,8 +102,14 @@ extern uint8_t pop_flash_time; #define POP_FLASH_YELLOW 0 #define POP_FLASH_RED 1 #define POP_FLASH_GREEN 2 +#define POP_FLASH_WHITE 3 /* тень: слияние и её смерть (уровень 12) */ extern uint8_t pop_flash_color; +/* add_life (seg000:11EC): +1 к ПОТОЛКУ HP и долив до него. Наружу — ради + * слияния с тенью (уровень 12, guards.c в другом банке): большая склянка + * зовёт ту же ветку изнутри модуля. */ +void pop_add_life(void) __banked; + /* is_feather_fall: кадров, пока действует зелье медленного падения. Читают * play_seq (опкод JMP_IF_FEATHER — ветки stepfloat/bumpfloat в seqtbl) и * приложение (сброс на старте уровня, seg003:189). */ @@ -127,6 +146,20 @@ extern uint8_t pop_leave_timer; extern uint8_t pop_loose_modif[30]; void pop_loose_tick(void) __banked; void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */ +/* Отдать НЕЗАКОНЧЕННЫЕ loose уходящей комнаты в персистентный room_modif + + * список trob, чтобы они досчитались заочно (в оригинале loose и есть trob, + * а do_trobs крутит список независимо от drawn_room). Звать ПЕРЕД любой + * перезагрузкой комнаты, пока карта ещё описывает СТАРУЮ. */ +void pop_loose_leave_room(void) __banked; +/* СТАРТ/РЕСТАРТ уровня: забыть фазы БЕЗ сдачи (load_level сносит всё сам). + * Звать рядом с pop_trob_reset, до первого enter_room. */ +void pop_loose_forget(void) __banked; + +/* Персонаж стоит НА тайле ворот, значит их решётку надо вернуть ПОВЕРХ него + * (порт условия draw_tile_fore, seg008:0D15 — разбор в pop_map.c). Бары + * рисуются в колонке ch->curr_col + 1: графика ворот принадлежит правому + * краю своего тайла. Рисование — pop_fore_over_char (pop_bg.c). */ +uint8_t pop_gate_over_char(const pop_char_t *ch) __banked; /* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ). * Приложение: определить debris-приземление в комнате снизу. Снять (=0). */ diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index ddf5079..edce330 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -28,6 +28,18 @@ #include #include +/* Фазы loose-плит: массив ТЕКУЩЕЙ комнаты (pop_map) и персистентный per-room + * (pop_trob) плюс постановка trob. Объявлены здесь, а не включением обоих + * заголовков: нужна ровно одна ячейка каждому — падающий кусок срывает + * плиту, в которую врезался (loose_fall, см. mob_break_loose). */ +extern uint8_t pop_loose_modif[30]; +uint8_t *pop_trob_modif(uint8_t room) __banked; +void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked; +/* remove_loose для ОДНОГО тайла — снять плиту с карты немедленно (честный + * loose_fall, см. mob_break_loose). Живёт в pop_map: там рабочая копия + * тайлов комнаты и фазы плит. */ +void pop_loose_remove(uint8_t room, uint8_t tilepos) __banked; + /* ---- Тайлсет: подземелье / дворец ----------------------------------- * * Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один * и тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE. У нас так @@ -809,9 +821,17 @@ void pop_loose_bake_empty(int row, int col) __banked * куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе * подряд, и второй спавн затирал состояние первого — его пиксели оставались * висеть в воздухе (на каждой странице на своей y → мерцание), а щебень - * появлялся только под одной плитой. 4 слота: больше одновременно падающих - * плит в одной комнате практически не бывает (в оригинале 14). */ -#define MOB_MAX 4 + * появлялся только под одной плитой. + * + * Слотов СТОЛЬКО ЖЕ, СКОЛЬКО В ОРИГИНАЛЕ (14). Было 4 с обоснованием + * «больше одновременно падающих плит в одной комнате практически не + * бывает» — неверным ровно с уровня 13: check_fall_flo роняет ШЕСТЬ разом + * (комнаты 23 и 16), а с учётом плит, которые эти куски сбивают по дороге + * (loose_fall), в воздухе бывает и больше. Лишний кусок молча терялся + * (`if (!m) return`), то есть тайл исчезал, а щебня на месте посадки не + * появлялось — найдено прогоном 2026-08-13, комната 16, тайл (1,5). + * Цена — 150 байт W2. */ +#define MOB_MAX 14 typedef struct { uint8_t active; /* 0 нет; 1 падает */ uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */ @@ -823,7 +843,12 @@ typedef struct { int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */ } mob_t; -static mob_t mobs[MOB_MAX]; +/* НЕ static намеренно: статики банкового модуля в карту символов не + * попадают, и разобрать зависший кадр по слотам (y каждого куска, порядок, + * defer) в отладчике нечем — адреса просто нет. Снято 2026-08-13 для + * разбора артефактов множественного падения; наружу таблица не объявлена и + * никем не читается. */ +mob_t mobs[MOB_MAX]; /* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на * который он лёг → приложение/pop_map ставит debris + do_knock. */ @@ -837,7 +862,7 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254}; static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25}; #define MOB_Y_NONE 0x7FFF -void pop_loose_mob_spawn(int row, int col) __banked +static void mob_spawn(uint8_t room, int row, int col) { uint8_t i; mob_t *m = 0; @@ -850,7 +875,7 @@ void pop_loose_mob_spawn(int row, int col) __banked m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */ m->row = (int8_t)row; m->col = (int8_t)col; - m->room = pop_t_room; /* curmob.room (add_mob, seg007:1010) */ + m->room = room; /* curmob.room (add_mob, seg007:1010) */ m->speed = 0; m->active = 1; m->clean = 0; @@ -858,6 +883,55 @@ void pop_loose_mob_spawn(int row, int col) __banked m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; } +/* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */ +void pop_loose_mob_spawn(int row, int col) __banked +{ + mob_spawn(pop_t_room, row, col); +} + +/* Копия куска ОТ ПОЗИЦИИ УДАРА — порт хвоста loose_fall (seg007:1085): + * curmob.y += 6; mob_down_a_row(); add_mob(); + * то есть новый кусок заводится не из тайла, а из места, где его сбили: + * на 6 пикселей ниже и рядом ниже, со скоростью УЖЕ ПОДЕЛЁННОЙ пополам. + * Через границу комнаты — как mob_down_a_row: y −= 192, ряд 0, комната снизу. */ +static void mob_spawn_copy(const mob_t *src) +{ + uint8_t i; + mob_t *m = 0; + int16_t y = (int16_t)(src->y + 6); + int8_t row = (int8_t)(src->row + 1); + uint8_t room = src->room; + + if (row >= 3) { + y = (int16_t)(y - 192); + row = 0; + room = pop_room_link(room, 3 /* вниз */); + if (!room) return; /* ушло из уровня — куску негде лететь */ + } + for (i = 0; i < MOB_MAX; i++) + if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; } + if (!m) return; + m->x = src->x; + m->y = y; + m->row = row; + m->col = src->col; + m->room = room; + m->speed = src->speed; /* уже half (loose_fall делит ДО add_mob) */ + m->active = 1; + m->clean = 0; + m->defer = 0; + m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; +} + +/* Отрыв куска в ЧУЖОЙ комнате: плита, которую Кид расшатал и убежал, всё + * равно проваливается (animate_loose в pop_trob крутит её trob по своей + * комнате, как do_trobs оригинала). Летит и садится кусок дальше сам — + * mob_tick_one давно умеет чужую комнату (mob_tile_at / pop_loose_exit). */ +void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked +{ + mob_spawn(room, row, col); +} + /* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26) * с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46: * ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину @@ -905,6 +979,29 @@ static uint8_t mob_tile_at(mob_t *m) return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F); } +/* loose_fall (seg007:1082) — падающий кусок пробил ЕЩЁ ОДНУ расшатанную + * плиту. Порт дословный, три действия В ОДНОМ КАДРЕ: + * 1. налетевший кусок тормозится вдвое (`curmob.speed >>= 1`); + * 2. сбитый тайл снимается с карты немедленно (`remove_loose`); + * 3. на месте удара заводится ВТОРОЙ кусок — копия налетевшего, сдвинутая + * на 6 пикселей и ряд вниз (`curmob.y += 6; mob_down_a_row; add_mob`). + * То есть в столбце дальше летят ДВА куска, и это правильное поведение + * оригинала, а не дефект. + * + * Здесь стоял обходной путь: плите ставилась фаза «падает следующим кадром», + * а всю работу делал pop_loose_tick. Цена оценивалась как «один кадр + * задержки» и оказалась выше: защита «один сигнал pop_loose_fell на кадр» + * придерживает провал, пока главный цикл не разберёт предыдущий, и на 13-м + * уровне (плиты сыплются пачкой) вторая плита появлялась через несколько + * кадров — уже после того, как первая улетела с экрана, — да ещё и с высоты + * покоя тайла, а не от места удара (найдено прогоном 2026-08-13). */ +static void mob_break_loose(mob_t *m) +{ + m->speed >>= 1; + pop_loose_remove(m->room, (uint8_t)(m->row * 10 + m->col)); + mob_spawn_copy(m); +} + static void mob_tick_one(mob_t *m, uint8_t pg) { uint8_t here; @@ -921,6 +1018,19 @@ static void mob_tick_one(mob_t *m, uint8_t pg) * на одной из двух страниц дабл-буфера — «застрявшая плита», видимая * через кадр (BUG-MOB-STALE-PAGE). */ if (m->prev_y[pg] != MOB_Y_NONE) { + /* Ширина/высота — по СЛЕДУ, а не по спрайту. Спрайт куска целиком + * влезает в 64x32 (41: 32x13 с y-3, 42: 26x15 с x+32,y-1, 43: 32x3), + * но mob_render рисует ещё и ЦЕЛЫЙ соседний тайл поверх правой части + * (окклюзия грани пола), причём в GFX_BANK_SPRITE — то есть мимо + * теневой копии фона. Всё, что тот тайл нарисовал вне коридора + * heal'а, не стиралось никогда: кусок улетал, а в соседнем столбце + * оставался обрезок пола (найдено прогоном 2026-08-13). Коридор + * расширен до площади, которую реально может тронуть эта + * перерисовка: столбец соседа по ширине и ряд по высоте. + * + * ЗАДЕЛ: правильнее не перерисовывать соседа вовсе, а КЛИПОВАТЬ + * правую часть куска — оригинал так и делает (add_mob_to_objtable, + * seg007:1161: clip.right = 40). Тогда коридор вернётся к 64x32. */ pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); m->prev_y[pg] = MOB_Y_NONE; } @@ -929,11 +1039,19 @@ static void mob_tick_one(mob_t *m, uint8_t pg) if (m->speed < 29) m->speed += 3; m->y += m->speed; /* move_loose (seg007:1126): низ куска пересёк границу своего ряда — - * смотрим тайл этого ряда. Пусто/loose — валится дальше (порт - * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); - * пол — приземление (loose_land: debris + do_knock). */ + * смотрим тайл этого ряда. Пусто/loose — валится дальше, пол — + * приземление (loose_land: debris + do_knock). Через LOOSE кусок именно + * ПРОЛЕТАЕТ, как через пустоту, — но по дороге СРЫВАЕТ её (loose_fall, + * seg007:1082). + * + * Здесь раньше стояло «порт loose_fall не нужен: в ряду ниже её нет» — + * допущение, верное для уровней 1-11 и сломавшееся на 13-м: комната 16, + * плиты (1,6) и (1,7), на которые сверху сыплется гряда из комнаты 1. + * Куски пролетали сквозь них правильно, а сами плиты оставались целыми и + * нарисованными (найдено прогоном 2026-08-13). */ if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { uint8_t t = mob_tile_at(m); + if (t == 11) mob_break_loose(m); if (t == 0 || t == 11) { /* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда — * в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает @@ -953,6 +1071,14 @@ static void mob_tick_one(mob_t *m, uint8_t pg) } here = (uint8_t)(m->room == pop_t_room); } + } else if (!here && pop_loose_exit) { + /* Сигнал прошлой посадки в чужой комнате ещё не разобран (он + * ОДИН на кадр) — держим кусок на границе ряда и садим следующим + * кадром, как это уже сделано для pop_loose_landed ниже. Без + * этого второй кусок затирал первый, и из нескольких плит, + * улетевших в одну дыру, щебень оставляла только одна (найдено + * прогоном 2026-08-13, уровень 13). */ + m->y = MOB_Y_BOUND[m->row + 1]; } else if (!here) { /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, но щебень и * КНОПКА под куском обязаны сработать — сигналим главному циклу @@ -996,9 +1122,47 @@ static void mob_tick_one(mob_t *m, uint8_t pg) if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */ else if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col); else over = (m->y > pop_cd[POP_CH_KID].fpy); - if (over) { m->defer = 1; return; } + /* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок + * пойдёт. Пока heal и вывод шли в одной итерации, heal СЛЕДУЮЩЕГО + * куска срезал верхнюю кромку уже нарисованного предыдущего: на 13-м + * уровне куски идут лесенкой в соседних столбцах с разницей ~20 px + * по y, и перекрытие гарантировано (артефакт «чёрный бар», найден + * прогоном 2026-08-13, комната 23). Теперь сперва тикают ВСЕ куски + * (то есть heal'ятся все), и только потом рисуются. */ + m->defer = over; } - mob_render(m, pg); +} + +/* Один проход отрисовки кусков. want_defer: 0 — те, что под Kid (идут сразу + * после фона), 1 — те, что поверх него (после персонажей). + * + * Порядок внутри прохода — ПО y, но ПО УБЫВАНИЮ: сперва нижние, последним + * верхний. Это НЕ описка и не общее правило «ниже = поверх»: сравнение двух + * объектов в оригинале (compare_curr_objs, seg008:1572) разводит два случая, + * + * оба объекта — падающие плиты (тип 0x80): y1 < y2 → по УБЫВАНИЮ + * любая другая пара: y1 > y2 → по возрастанию + * + * то есть между СОБОЙ куски укладываются наоборот, чем всё остальное: верхний + * ложится поверх нижнего. Видно на паре из loose_fall (сбитая плита летит в + * 6 пикселях под налетевшей): при общем правиле нижняя перекрывала верхнюю, + * и это поймал прогон 2026-08-13. + * + * Порядок «кусок против Кида» здесь ни при чём — его решает defer выше, и там + * действует обычное правило. */ +static void mob_draw_pass(uint8_t pg, uint8_t want_defer) +{ + uint8_t order[MOB_MAX], n = 0, i, j; + for (i = 0; i < MOB_MAX; i++) { + mob_t *m = &mobs[i]; + if (!m->active || m->room != pop_t_room) continue; + if ((uint8_t)(m->defer != 0) != want_defer) continue; + for (j = n; j > 0 && mobs[order[j - 1]].y < m->y; j--) + order[j] = order[j - 1]; + order[j] = i; + n++; + } + for (i = 0; i < n; i++) mob_render(&mobs[order[i]], pg); } void pop_loose_mob_tick(void) __banked @@ -1010,6 +1174,15 @@ void pop_loose_mob_tick(void) __banked * (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а * стереть себя ему всё равно надо будет. */ if (mobs[i].active || mobs[i].clean) + /* Метка «фон трогали вот здесь» обязана покрывать ВЕСЬ коридор + * heal'а, а не габарит спрайта. Иначе персонаж, попавший в + * расширенную часть коридора, стирается heal'ом, но перерисовать + * себя не просит (pop_char_skip_mask его пропускает) — и просто + * исчезает с экрана. Ровно это и случилось, когда коридор + * расширили под транзитную перерисовку соседа (прогон + * 2026-08-13: в комнате 23 после падения гряды пропадал Кид). + * Запас сверху — на путь, пройденный за кадр (speed до 29): heal + * стирает ПРОШЛУЮ позицию, она выше текущей. */ pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64); mob_tick_one(&mobs[i], pg); } @@ -1042,6 +1215,24 @@ static void mob_render(mob_t *m, uint8_t pg) * границе рядов. Порт draw_mob (seg007:13E5). */ int8_t r = pop_y_to_row((int16_t)m->y); int8_t rt = pop_y_to_row((int16_t)(m->y - 18)); + /* ОКНО = КОРИДОР HEAL ЭТОГО КУСКА, ни пикселем шире. draw_tile + * рисует тайл ЦЕЛИКОМ (пол, грани, верх кладки ряда ниже — порядка + * 64x63), причём в GFX_BANK_SPRITE, то есть мимо теневой копии фона. + * Всё, что он клал за пределами коридора, не стиралось НИКОГДА: + * кусок улетал, а в соседнем столбце оставался обрезок пола + * (найдено прогоном 2026-08-13). + * + * Расширять под это heal НЕЛЬЗЯ: heal — самая дорогая операция + * кадра, а на 13-м уровне таких кусков одновременно шесть. Поэтому + * режем не heal под перерисовку, а перерисовку под heal — тем же + * окном, которым fore-слой отсекает лишние тайлы. */ + pop_t_fclip_x0 = MOB_X0(m->x); + pop_t_fclip_x1 = MOB_X0(m->x) + MOB_W; + pop_t_fclip_y0 = m->y - 27 + POP_YOFF; + pop_t_fclip_y1 = pop_t_fclip_y0 + 32; + pop_t_fclip_ly0 = pop_t_fclip_y0; /* переворота у кусков нет */ + pop_t_fclip_ly1 = pop_t_fclip_y1; + pop_t_fclip_on = 1; if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */ draw_tile(r, m->col + 1); pop_t_clip_top = 0; @@ -1050,19 +1241,30 @@ static void mob_render(mob_t *m, uint8_t pg) draw_tile(rt, m->col + 1); pop_t_clip_top = 0; } + pop_t_fclip_on = 0; } gfx_set_bank(GFX_BANK_NORMAL); /* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */ pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30); } +/* Отрисовка кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed(): фон + * кадра обязан быть полностью готов до любого спрайта. + * + * Раньше куски рисовались внутри pop_loose_mob_tick, то есть ДО разбора + * пометок перерисовки, и перепечатка тайла ложилась поверх уже нарисованного + * куска — окно задней стены, передняя грань пола (найдено прогоном + * 2026-08-13, комнаты 16 и 23). */ +void pop_loose_mob_draw(void) __banked +{ + mob_draw_pass(gfx_get_draw_page() & 1, 0); +} + /* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать * после отрисовки персонажей, до fore-слоя. */ void pop_loose_mob_draw_over(void) __banked { - uint8_t pg = gfx_get_draw_page() & 1, i; - for (i = 0; i < MOB_MAX; i++) - if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); } + mob_draw_pass(gfx_get_draw_page() & 1, 1); } /* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index 615f83b..9a2ca60 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -81,3 +81,24 @@ void pop_dbg_trap(void) { pop_dbg_hits++; } /* Зеркало уровня 4 — см. pop_state.h. */ int8_t pop_jumped_mirror; + +/* Тайл СОСЕДНЕЙ комнаты изменился, пока она не отрисована (плита, которую + * Кид расшатал и бросил, провалилась заочно — animate_loose в pop_trob). + * Кромочные колонки и у отрисовки (lcol_fg), и у коллизии (g_lcol/g_rcol) — + * СНИМКИ на момент входа в комнату, поэтому их надо перечитать. Ставит + * банк 6, разбирает главный цикл. */ +uint8_t pop_neigh_dirty; + +/* seamless (seg000:900 + seg003:158): переход 12 → 13 идёт БЕЗ двери + * уровня — уровень кончается фактом попадания в комнату 23, и следующий + * начинается в комнате с тем же номером и тем же рядом. Флаг говорит + * старту уровня «не сбрасывай HP» (и, когда появятся заставки, «не + * показывай „LEVEL 13“»): Кид продолжает бежать, не заметив границы. + * Ставит главный цикл, гасит pop_start_level. */ +uint8_t pop_seamless; + +/* «Кид попал в комнату ЧИТОМ навигации» — держит бесшовный переход уровня. + * Отладочная добавка, у оригинала телепорта между комнатами нет вовсе; см. + * pop_dbg_roomnav и ../docs/impl_diff.md. Снимается первой же СМЕНОЙ + * КОМНАТЫ ОБЫЧНЫМ ХОДОМ и стартом уровня. */ +uint8_t pop_nav_hold; diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h index 29e3ff1..07dcbc0 100644 --- a/applications/PoP/roomtest/pop_state.h +++ b/applications/PoP/roomtest/pop_state.h @@ -20,6 +20,19 @@ extern uint8_t pop_loose_landed; extern uint8_t pop_loose_exit; extern uint8_t pop_loose_exit_room; +/* Тайл СОСЕДНЕЙ (не отрисованной) комнаты изменился — перечитать кромки. + * Подробности — в pop_state.c. */ +extern uint8_t pop_neigh_dirty; + +/* Бесшовный переход между уровнями (seamless): подробности в pop_state.c. + * Ставится главным циклом при попадании в комнату-выход, гасится стартом + * уровня — он же единственный, кто на флаг смотрит. */ +extern uint8_t pop_seamless; + +/* Держатель бесшовного перехода после ЧИТ-телепорта (см. pop_state.c). + * Ставит pop_dbg_roomnav, снимает первая смена комнаты обычным ходом. */ +extern uint8_t pop_nav_hold; + /* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона * (боёвка в банке, пики в pop_map), переключает главный цикл. */ extern uint8_t pop_immortal; diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index fb9dd2a..2a322e0 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -14,6 +14,7 @@ * пометки, поэтому pop_process_trobs — единственная функция модуля, которую * нельзя звать из __banked-кода. */ #include "pop_bg.h" +#include "pop_tile.h" /* pop_palace — тип уровня как модификатор дыры */ #include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */ #include "pop_kid.h" /* Kid.curr_row/alive — условие остановки чомпера */ #include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */ @@ -31,6 +32,7 @@ #define TILE_CHOMP 0x12 /* чомпер (челюсти) */ #define TILE_TORCH 0x13 #define TILE_TORCH_DEBRIS 0x1E +#define TILE_EMPTY 0x00 /* remove_loose: на месте провалившейся плиты */ /* ---- Персистентное per-room modifier-состояние -------------------- */ static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES]; @@ -305,6 +307,54 @@ static void animate_spike(uint8_t *m, int8_t *type) *m = mod; } +/* animate_loose (seg007:0CBA) — для плиты в НЕ отрисованной комнате. + * + * ЗАЧЕМ. В оригинале loose-пол — обычный trob, а do_trobs крутит весь + * список независимо от drawn_room: расшатанная плита доваливается, даже если + * Кид уже убежал в соседнюю комнату. У нас фаза живёт в pop_loose_modif[30] + * — массиве ТЕКУЩЕЙ комнаты, — и смена комнаты её обнуляла: плита, + * пробежав по которой Кид уходил вправо, оставалась целой (уровень 10, + * комната 2, тайл 2,9). + * + * Разделение обязанностей: пока комната отрисована, фазой владеет + * pop_loose_tick (ему нужны перерисовка, звук, mob-порядок), и trob здесь + * сразу снимается. На выходе из комнаты pop_loose_leave_room (pop_map.c) + * сбрасывает фазу в ПЕРСИСТЕНТНЫЙ room_modif и заводит trob — дальше плита + * досчитывает здесь, а на входе обратно pop_loose_reset забирает фазу + * назад. Персистентный модификатор и есть curr_room_modif оригинала, так + * что расхождения с SDLPoP нет — только двухуровневое хранение. + * + * Рисовать тут нечего (комната не на экране), поэтому от оригинала остаётся + * ровно счёт фазы и провал: тайл в EMPTY + отрыв падающего куска. */ +static void animate_loose(uint8_t *m, int8_t *type, uint8_t room, uint8_t tp, + uint8_t cur_room) +{ + uint8_t ph; + if (room == cur_room) { *type = -1; return; } /* хозяин — pop_loose_tick */ + ph = (uint8_t)(*m + 1); + if (ph == 0) ph = 1; /* перескок нуля, см. pop_loose_tick */ + if (ph & 0x80) { /* тряска (do_knock): 0x80..0x83 */ + /* Спецкейс уровня 13 (seg007:823) — тот же, что в pop_loose_tick: + * отложенный старт от check_fall_flo живёт в старшем бите, и гасить + * его нельзя. Здесь он тоже нужен: гряда лежит в комнате СВЕРХУ, и + * пока Кид не вошёл в комнату под ней, счёт идёт заочно. */ + if (pop_current_level == POP_LOOSE_TILES_LEVEL) { /* ждём молча */ } + else if (ph >= 0x84) { ph = 0; *type = -1; } + } else if (ph >= 11) { /* loose_floor_delay — провал */ + /* remove_loose (seg007:0EB8): тайл → EMPTY, модификатором пустой + * клетки становится ТИП УРОВНЯ (по нему draw_tile_right выбирает + * blueline у дыры). Тот же порядок, что в pop_loose_tick. */ + pop_level_set_tile(room, tp, TILE_EMPTY); + /* Комната не на экране, но её колонка 9 ВИДНА в шве текущей и лежит + * в кромочных снимках — попросить главный цикл их перечитать. */ + pop_neigh_dirty = 1; + pop_loose_mob_spawn_at(room, TP_ROW[tp], TP_COL[tp]); + ph = pop_palace; + *type = -1; + } + *m = ph; +} + /* ---- Кнопки/ворота (seg007) --------------------------------------- * * Таблицы связей LINKLOC/LINKMAP — в pop_level (мутабельная копия в W2). * doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */ @@ -574,6 +624,9 @@ void pop_process_trobs(uint8_t cur_room) __banked case TILE_GATE: animate_door(&mod[tp], &type); break; + case TILE_LOOSE: + animate_loose(&mod[tp], &type, room, tp, cur_room); + break; case TILE_LEVELDOOR: animate_leveldoor(&mod[tp], &type, cur_room); break; diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h index f36b2bd..4c8747e 100644 --- a/applications/PoP/roomtest/pop_tune.h +++ b/applications/PoP/roomtest/pop_tune.h @@ -126,4 +126,29 @@ * чтение 34 страниц пришлась на загрузку уровня, а не на глоток зелья. */ #define POP_UPSIDE_LEVEL 9 +/* Уровень ПАДАЮЩИХ ПЛИТ (custom->loose_tiles_*, seg000:1317 check_fall_flo). + * При входе в комнату 23 или 16 тринадцатого уровня вся гряда loose ряда 2 + * комнаты СВЕРХУ получает ОТРИЦАТЕЛЬНУЮ фазу — отложенный старт, — и плиты + * сыплются на Кида вразнобой. Комнаты сверху — 17 и 1 соответственно, и в + * них ряд 2 колонок 2..7 действительно loose (сверено по res2013.bin, + * ../docs/levels_12_15_plan.md §7.1). Диапазон 22..27 — надмножество: в + * комнате 1 тайл 24 пустой, и он молча пропускается. + * + * Комната 23 у этого уровня ещё и СТАРТОВАЯ, то есть плиты сыплются с + * первого кадра уровня — это его открывающая сцена, а не событие в + * середине. */ +#define POP_LOOSE_TILES_LEVEL 13 +#define POP_LOOSE_TILES_ROOM1 23 +#define POP_LOOSE_TILES_ROOM2 16 +#define POP_LOOSE_TILES_FIRST 22 /* ряд 2, колонка 2 */ +#define POP_LOOSE_TILES_LAST 27 /* ряд 2, колонка 7 */ + +/* Бесшовный выход уровня (tbl_seamless_exit, play_level_2 seg000:900). У + * оригинала это таблица по уровням, но заполнена она ровно одной парой: + * уровень 12 кончается, как только Кид попал в комнату 23 — двери уровня у + * него нет. Комната 23 у 13-го уровня СТАРТОВАЯ и совпадает геометрией + * ряда, поэтому шва не видно (../docs/levels_12_15_plan.md §1.7). */ +#define POP_SEAMLESS_LEVEL 12 +#define POP_SEAMLESS_ROOM 23 + #endif /* POP_TUNE_H */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index f5c62e9..2dfbcbb 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -355,6 +355,8 @@ int main(void) } pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */ + pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */ + pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_ctrl_tick(); /* ввод -> control(): смена seq */ /* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы — @@ -396,6 +398,7 @@ int main(void) pop_next_level = (uint8_t)(pop_current_level + 1); } else if (down) { /* L2: переход в комнату снизу — продолжает падать */ + pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */ enter_room(down); /* координаты Kid уже репроецированы pop_map */ gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */ } else { /* выпал из уровня (нет комнаты снизу) — рестарт */ @@ -424,9 +427,20 @@ int main(void) pop_checkpoint = 1; hitp_beg_lev = hitp_max; } + /* Соседи чекпойнта по leave_room (seg002:0490): в ветке ухода + * ВПРАВО (pop_leave_dir == 2) — sword_disappears + meet_Jaffar, + * в ветке ВЛЕВО — Jaffar_exit. Комната во всех трёх ещё СТАРАЯ, + * как и в оригинале (goto_other_room идёт после). */ + if (pop_leave_dir_prev == 2) { + pop_sword_disappears(); + pop_meet_jaffar(); + } else if (left) { + pop_jaffar_exit(); + } if (nb) { /* side для pop_guard_follow — то же направление, по которому * ищем соседа (roomleave_result оригинала). */ + pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */ enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1)); gfx_set_draw_page(back); } @@ -438,6 +452,30 @@ int main(void) /* — ДО Kid, чтобы Kid всегда был поверх плиты) */ pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */ /* process_trobs — рисования внутри нет) */ + if (pop_neigh_dirty) { + /* Тайл СОСЕДА изменился заочно (провалилась брошенная плита — + * animate_loose). Обе кромочные копии — снимки на момент входа + * в комнату, обновляем обе: g_lcol/g_rcol (коллизия у шва) и + * lcol_fg (отрисовка: колонка 9 левого соседа видна в col0 + * через draw_tile_anim_right, pop_room.c). Без второй шов + * продолжал рисовать плиту, которой уже нет — LOOSE-ROOM-CHANGE. + * Событие редкое, поэтому по сигналу, а не опросом каждый кадр. */ + uint8_t lft = pop_room_link(cur_room, 0); + pop_neigh_dirty = 0; + pop_map_refresh_edges(); + if (lft) { + uint8_t i; + pop_level_access_begin(); + for (i = 0; i < 3; i++) { + uint8_t c = pop_level_tile_raw(lft, (uint8_t)(9 + i * 10)); + if (c == (uint8_t)(lcol_fg[i] & 0x1F)) continue; + lcol_fg[i] = c; + seam_rows |= (uint8_t)(1 << i); + seam_redraw = 2; /* обе страницы дабл-буфера */ + } + pop_level_access_end(); + } + } pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */ /* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */ { /* ворота соседа слева (room8) видны в col0 — */ @@ -471,6 +509,18 @@ int main(void) } } } + /* Бесшовный выход уровня 12 (play_level_2, seg000:900): двери уровня + * у него нет — уровень кончается ФАКТОМ попадания в комнату 23. + * Условие «уровень ещё не сменён» обязательно: комната 23 есть и у + * 13-го, причём она там СТАРТОВАЯ, и без гарда переход зациклился бы + * (12 → 13 → 14 в первом же кадре нового уровня). */ + if (pop_current_level == POP_SEAMLESS_LEVEL && + pop_next_level == pop_current_level && + !pop_nav_hold && /* не по чит-телепорту, см. pop_state.c */ + kid_room == POP_SEAMLESS_ROOM) { + pop_next_level = (uint8_t)(pop_current_level + 1); + pop_seamless = 1; + } if (pop_next_level != pop_current_level) { /* Смена уровня целиком — в банке (pop_level_switch): грузится * файл, спрайты соперника и тайлсет. Раз на уровень. */ @@ -550,6 +600,7 @@ int main(void) * HP — красный, перо (медленное падение) — зелёный. */ if (pop_flash_color == POP_FLASH_RED) flash_bg(255, 0, 0); else if (pop_flash_color == POP_FLASH_GREEN) flash_bg(85, 255, 85); + else if (pop_flash_color == POP_FLASH_WHITE) flash_bg(255, 255, 255); else flash_bg(255, 255, 85); pop_flash_time--; if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW; @@ -567,6 +618,11 @@ int main(void) * пролетел сквозь зеркало, тень должна родиться ДО того, как слот * соперника пойдёт рисоваться, иначе кадр её проглотит. */ pop_check_mirror(); + /* Падающие плиты, идущие ПОД Kid. Здесь, а не внутри pop_loose_tick: + * фон кадра (перепечатки тайлов, шов) обязан быть готов ДО любого + * спрайта, иначе перепечатка ложится поверх уже нарисованного куска — + * так окно задней стены и передняя грань пола оказывались на плите. */ + pop_loose_mob_draw(); { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль * персонажа (см. guard_over_kid). Отрисовка одна на всех Char * (pop_cdraw.c), различает их только слот. */ diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 2b77fc6..3dac996 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -109,7 +109,15 @@ void pop_start_level(void) __banked pop_level_reset_tiles(); pop_level_reset_guards(); pop_guard_reset(); /* start_level: Guard.direction = none */ + /* united_with_shadow = 0 (seg003:95) — состояние ПЕРСОНАЖА уровня, а не + * комнаты: pop_guard_reset зовётся и при каждом входе в комнату (из + * pop_guard_enter), поэтому гасить слияние надо здесь. */ + pop_united_shadow = 0; + pop_shadow_init = 0; + pop_guard_notice_timer = 0; /* фора Джафара — не переживает рестарт */ + pop_nav_hold = 0; /* старт уровня снимает и держатель чита */ pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ + pop_loose_forget(); /* ...и фазы плит — уровень восстановлен целиком */ /* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в * комнату-чекпойнт после респавна остался бы непройденным. */ if (chkp) @@ -155,7 +163,11 @@ void pop_start_level(void) __banked /* 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(); + /* seamless (seg003:158): пришли БЕЗ двери уровня, прямо на бегу — HP не + * трогаем, оно переезжает как есть. Флаг одноразовый: у оригинала его + * гасит заставка «LEVEL N», которую этот же переход и пропускает. */ + if (pop_seamless) { pop_seamless = 0; pop_kid_hp_carry(); } + else pop_kid_hp_reset(); } /* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- * @@ -234,6 +246,16 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked holding_sword = 0; offguard = 0; guard_refrac = 0; + + /* ПРОВАЛ НА СЛЕДУЮЩИЙ УРОВЕНЬ ПО ЧИТУ (та же болезнь, что выше). + * Уровень 12 кончается ФАКТОМ попадания в комнату 23 — двери у него нет + * (pop_seamless, seg000:0900). А навигация перебирает комнаты ПО + * НОМЕРУ, то есть любой обход рано или поздно наступает на 23-ю, и + * уровень молча меняется на 13-й: комнату 23 двенадцатого уровня читом + * не посмотреть в принципе, а прогон «съезжает» на соседний уровень + * незаметно для проверяющего (поймано 2026-08-13 на первом же прогоне). + * Держим триггер, пока Кид не сменит комнату НОГАМИ. */ + pop_nav_hold = 1; return 1; } @@ -276,6 +298,10 @@ void enter_room_side(uint8_t room, uint8_t side) __banked * enter звать нельзя: он уже перенесён и живёт дальше как есть. */ uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0); if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */ + /* ...и так же запомнить НЕДОСЧИТАННЫЕ loose: пока карта ещё описывает + * СТАРУЮ комнату, фаза уезжает в персистентный room_modif + trob, и + * плита доваливается заочно (см. pop_loose_leave_room / animate_loose). */ + pop_loose_leave_room(); uint8_t *room_mod; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_map_set(room_fg); @@ -317,6 +343,7 @@ void enter_room_side(uint8_t room, uint8_t side) __banked pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */ + pop_check_fall_flo(); /* спецсобытие ур.13: гряда плит сверху */ /* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»- * уровнях забирает слот соперника себе: вернула 1 — стража поднимать * НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index 64c8ead..9d2d17f 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -49,6 +49,12 @@ OBJS_char := $(OBJS_phys) # (pop_check_mouse) и общей физикой соперника — то есть ровно то, что в # остальных наборах заменено ручным прогоном play_guard_frame. OBJS_mouse := $(OBJS_phys) build/eng_guards.rel +# t_shadow — спецсобытия уровня 12 (встреча с тенью, общий урон, слияние). +# guards.c нужен по той же причине, что и мыши: check_shadow/ +# check_killed_shadow/sword_disappears живут в нём. +OBJS_shadow := $(OBJS_phys) build/eng_guards.rel +# t_jaffar — спецсобытия уровня 13 (встреча, победа, выход, падающая гряда). +OBJS_jaffar := $(OBJS_phys) build/eng_guards.rel include $(TESTKIT)/host-tests.mk diff --git a/applications/PoP/roomtest/tests-host/scene.c b/applications/PoP/roomtest/tests-host/scene.c index 0c306e6..e501d60 100644 --- a/applications/PoP/roomtest/tests-host/scene.c +++ b/applications/PoP/roomtest/tests-host/scene.c @@ -46,12 +46,18 @@ void sc_room(const uint8_t *fg, uint8_t room) void sc_kid_at(uint8_t col, uint8_t row, int8_t dir) { kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir); + /* kid_init комнату не трогает (в продукте её ставит enter_room_side), а + * сцена обязана быть согласованной: логика, спрашивающая «персонаж в + * ОТРИСОВАННОЙ комнате?» (pop_gate_over_char), иначе всегда отвечает + * «нет». Тест, которому нужна другая комната, задаёт её сам. */ + Kid.room = tk_start_room; pop_kid_hp_reset(); } void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir) { kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir); + Kid.room = tk_start_room; Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */ /* Колонку пересчитываем В ОКНЕ Char: determine_col — часть физики, а та * работает с активным персонажем (порт load_fram_det_col). */ diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index 2b5a47f..ea76f95 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -223,12 +223,12 @@ uint8_t pop_level_guard_hp(uint8_t level) __banked { (void)level; return 3; } /* Тайлы «уровня» для соседних комнат: pop_map_set_edges забирает fg соседей * слева/справа сам (кэш на входе в комнату), значит тесту нужен источник. * Заполняет сам тест — по умолчанию комнат нет и всё стена. */ -uint8_t tk_room_fg[TK_ROOMS][30]; +uint8_t tk_room_fg[TK_ROOMS + 1][30]; /* индекс = НОМЕР комнаты (1..TK_ROOMS) */ void pop_level_access_begin(void) { } void pop_level_access_end(void) { } uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos) { - if (room == 0 || room >= TK_ROOMS || tilepos >= 30) return 20; /* стена */ + if (room == 0 || room > TK_ROOMS || tilepos >= 30) return 20; /* стена */ return tk_room_fg[room][tilepos]; } @@ -244,7 +244,7 @@ uint8_t pop_level_tile(uint8_t room, uint8_t tilepos) * пор, как do_pickup правит страницу уровня сразу (BUG-POTION-STRIPE). */ void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile) { - if (room == 0 || room >= TK_ROOMS || tilepos >= 30) return; + if (room == 0 || room > TK_ROOMS || tilepos >= 30) return; tk_room_fg[room][tilepos] = tile; } diff --git a/applications/PoP/roomtest/tests-host/stubs.h b/applications/PoP/roomtest/tests-host/stubs.h index 2185277..ed71b8e 100644 --- a/applications/PoP/roomtest/tests-host/stubs.h +++ b/applications/PoP/roomtest/tests-host/stubs.h @@ -42,7 +42,11 @@ uint8_t tk_log_count(uint8_t kind); /* Состояние стражей по комнатам для follow_guard (см. stubs.c). */ -#define TK_ROOMS 8 +/* Комнат в таблице «уровня». 24 = столько же, сколько в настоящем уровне + * PoP: спецсобытия поздних уровней адресуют комнаты по их РЕАЛЬНЫМ номерам + * (меч тени лежит в 15-й, уход за ним — из 18-й), и обрезать их до восьми + * значило бы тестировать не тот адрес. Цена — 720 байт тестового ОЗУ. */ +#define TK_ROOMS 24 extern uint8_t tk_guard_room; /* комната, где страж есть «по данным уровня» */ extern uint8_t tk_gs_tile[TK_ROOMS]; extern uint16_t tk_gs_seq[TK_ROOMS]; @@ -52,6 +56,6 @@ void tk_guard_state_clear(void); extern uint8_t tk_link_down; /* fg комнат «уровня» — источник для кэша соседей в pop_map_set_edges. */ -extern uint8_t tk_room_fg[TK_ROOMS][30]; +extern uint8_t tk_room_fg[TK_ROOMS + 1][30]; /* индекс = номер комнаты */ #endif diff --git a/applications/PoP/roomtest/tests-host/t_char.c b/applications/PoP/roomtest/tests-host/t_char.c index 789665d..0a6c710 100644 --- a/applications/PoP/roomtest/tests-host/t_char.c +++ b/applications/PoP/roomtest/tests-host/t_char.c @@ -29,6 +29,7 @@ #define E 0 /* empty */ #define F 1 /* floor */ +#define G 4 /* gate (portcullis) */ #define W 20 /* wall */ /* Ровный пол на ряду 1 (как room_flat в t_phys). */ @@ -47,6 +48,13 @@ static const uint8_t room_drop2[30] = { W, F, F, F, F, F, F, F, F, F, }; +/* Решётка ворот в (2,6) — сцена под окклюзию персонажа, стоящего в проёме. */ +static const uint8_t room_gate[30] = { + W, E, E, E, E, E, E, E, E, E, + W, E, E, E, E, E, E, E, E, E, + W, F, F, F, F, F, G, F, F, F, +}; + /* ---- take_hp: кому достаётся урон ----------------------------------- */ TC_TEST(char_take_hp_goes_to_active) @@ -434,6 +442,57 @@ TC_TEST(char_skeleton_falls_out_elsewhere) tc_check(Guard.charid, 0, "скелет убран, как обычный выпавший", __LINE__); } +/* ---- Окклюзия воротами (GATE-FORE-KID) ------------------------------ * + * Персонаж, стоящий В ПРОЁМЕ ворот, обязан уходить ЗА решётку: оригинал + * кладёт её бары в foretable (draw_tile_fore, seg008:0D15). Решение — + * pop_gate_over_char; сама отрисовка живёт в слое фона и в харнесс не + * линкуется, поэтому проверяем именно РЕШЕНИЕ. */ + +TC_TEST(char_gate_covers_char_standing_in_it) +{ + sc_room(room_gate, 1); + sc_kid_at(6, 2, 0); /* ровно на тайле ворот */ + pop_loadkid(); + TC_TRUE(pop_gate_over_char(&Char)); +} + +TC_TEST(char_gate_ignores_neighbour_tiles) +{ + sc_room(room_gate, 1); + sc_kid_at(5, 2, 0); /* колонка слева от ворот */ + pop_loadkid(); + TC_EQ(pop_gate_over_char(&Char), 0); + sc_kid_at(7, 2, 0); /* и справа */ + pop_loadkid(); + TC_EQ(pop_gate_over_char(&Char), 0); +} + +TC_TEST(char_gate_ignores_other_room) +{ + /* Персонаж той же клетки, но ДРУГОЙ комнаты (страж, оставшийся в + * соседней): его решётка текущей комнаты не касается. */ + sc_room(room_gate, 1); + sc_kid_at(6, 2, 0); + pop_loadkid(); + Char.room = 5; + TC_EQ(pop_gate_over_char(&Char), 0); +} + +TC_TEST(char_gate_never_in_last_column) +{ + /* Колонка 9: бары легли бы в колонку 10, то есть в СОСЕДНЮЮ комнату — + * у оригинала это отсекает `Kid.room != room_R`. */ + static const uint8_t room_gate9[30] = { + W, E, E, E, E, E, E, E, E, E, + W, E, E, E, E, E, E, E, E, E, + W, F, F, F, F, F, F, F, F, G, + }; + sc_room(room_gate9, 1); + sc_kid_at(9, 2, 0); + pop_loadkid(); + TC_EQ(pop_gate_over_char(&Char), 0); +} + void main(void) { sc_init(); @@ -457,4 +516,8 @@ void main(void) TC_RUN(char_tiles_resolve_across_rooms); TC_RUN(char_skeleton_reappears_below); TC_RUN(char_skeleton_falls_out_elsewhere); + TC_RUN(char_gate_covers_char_standing_in_it); + TC_RUN(char_gate_ignores_neighbour_tiles); + TC_RUN(char_gate_ignores_other_room); + TC_RUN(char_gate_never_in_last_column); } diff --git a/applications/PoP/roomtest/tests-host/t_jaffar.c b/applications/PoP/roomtest/tests-host/t_jaffar.c new file mode 100644 index 0000000..08bf622 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_jaffar.c @@ -0,0 +1,349 @@ +/* + * t_jaffar — спецсобытия УРОВНЯ 13: встреча с Джафаром, победа, выход и + * три исключения loose-механики (падающая гряда плит). + * + * Своего ИИ у Джафара нет — это обычный страж (seg002:085A), поэтому + * проверять тут нечего кроме СОБЫТИЙ и ветвлений по номеру уровня. А + * именно они и опасны: три из них — это `if (level == 13)` посреди чужого + * кода (тряска плит, удар плитой в беге, «страж не встаёт в стойку»), и + * сломать их можно правкой, вообще не думая про 13-й уровень. + * + * Порт: seg002:0544 meet_Jaffar, seg002:0517 Jaffar_exit, + * seg006:1927 on_guard_killed, seg000:1317 check_fall_flo, + * seg007:823/949 исключения animate_loose/loose_make_shake. + */ +#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" +#include "pop_tune.h" +#include "scene.h" +#include "stubs.h" + +#define E 0 /* empty */ +#define F 1 /* floor */ +#define L 11 /* loose */ +#define W 20 /* wall */ + +/* Комната с полом внизу и грядой loose в ряду 2 — она же будет играть роль + * «комнаты сверху» для check_fall_flo. */ +static const uint8_t room_loose[30] = { + F, F, F, F, F, F, F, F, F, F, + W, W, W, W, W, W, W, W, W, W, + F, F, L, L, L, L, L, L, F, F, +}; + +/* Комната, в которую Кид входит снизу: ровный пол во всех рядах. */ +static const uint8_t room_flat[30] = { + F, F, F, F, F, F, F, F, F, F, + F, F, F, F, F, F, F, F, F, F, + F, F, F, F, F, F, F, F, F, F, +}; + +static uint8_t above_row[10]; + +static void scene_level13(uint8_t room) +{ + pop_guard_reset(); + pop_current_level = JAFFAR_LEVEL; + pop_leveldoor_open = 0; + pop_guard_notice_timer = 0; + sc_room(room_flat, room); + sc_kid_at(1, 2, 1); + tk_log_clear(); +} + +/* ---- meet_Jaffar ----------------------------------------------------- */ + +TC_TEST(jaffar_notice_timer_set_leaving_room3_right) +{ + scene_level13(JAFFAR_MEET_ROOM); + Kid.room = JAFFAR_MEET_ROOM; + pop_meet_jaffar(); + TC_EQ(pop_guard_notice_timer, JAFFAR_NOTICE_TIME); +} + +/* Дверь уровня уже открыта — значит Джафар мёртв и Кид возвращается. + * Второй форы не даётся. */ +TC_TEST(jaffar_notice_not_given_after_victory) +{ + scene_level13(JAFFAR_MEET_ROOM); + Kid.room = JAFFAR_MEET_ROOM; + pop_leveldoor_open = 2; + pop_meet_jaffar(); + TC_EQ(pop_guard_notice_timer, 0); +} + +TC_TEST(jaffar_notice_not_given_from_other_room) +{ + scene_level13(JAFFAR_MEET_ROOM); + Kid.room = (uint8_t)(JAFFAR_MEET_ROOM + 1); + pop_meet_jaffar(); + TC_EQ(pop_guard_notice_timer, 0); +} + +TC_TEST(jaffar_notice_is_level_gated) +{ + scene_level13(JAFFAR_MEET_ROOM); + pop_current_level = 12; + Kid.room = JAFFAR_MEET_ROOM; + pop_meet_jaffar(); + TC_EQ(pop_guard_notice_timer, 0); +} + +/* Счётчик убывает по кадру и останавливается на нуле. */ +TC_TEST(jaffar_notice_timer_counts_down) +{ + uint8_t i; + pop_united_shadow = 0; + pop_guard_notice_timer = 3; + for (i = 0; i < 5; i++) pop_frame_timers(); + TC_EQ(pop_guard_notice_timer, 0); +} + +/* ---- Джафар ЖДЁТ, пока идёт фора ------------------------------------- */ + +/* Поднять соперника с УБРАННЫМ мечом в одном ряду с Кидом и «показать» его: + * дальше решение принимает autocontrol_guard_inactive. */ +static void jaffar_faces_kid(void) +{ + sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6); + Guard.sword = 0; + can_guard_see_kid = 2; + is_guard_notice = 0; +} + +TC_TEST(jaffar_holds_still_while_notice_timer_runs) +{ + uint8_t i; + scene_level13(1); + jaffar_faces_kid(); + pop_guard_notice_timer = JAFFAR_NOTICE_TIME; + for (i = 0; i < 6; i++) { can_guard_see_kid = 2; pop_guard_tick(); } + /* Стойку не сменил: меч так и в ножнах. */ + TC_EQ(Guard.sword, 0); +} + +/* Фора кончилась — обычное поведение стража возвращается. */ +TC_TEST(jaffar_draws_sword_after_notice_timer) +{ + uint8_t i; + scene_level13(1); + jaffar_faces_kid(); + pop_guard_notice_timer = 0; + for (i = 0; i < 12; i++) { can_guard_see_kid = 2; pop_guard_tick(); } + TC_EQ(Guard.sword, SWORD_2_DRAWN); +} + +/* На прочих уровнях таймер ни на что не влияет: условие в оригинале + * двойное (`level != 13 || timer == 0`), и потерять первую половину легко. */ +TC_TEST(jaffar_notice_timer_ignored_on_other_levels) +{ + uint8_t i; + scene_level13(1); + pop_current_level = 12; + jaffar_faces_kid(); + pop_guard_notice_timer = JAFFAR_NOTICE_TIME; + for (i = 0; i < 12; i++) { can_guard_see_kid = 2; pop_guard_tick(); } + TC_EQ(Guard.sword, SWORD_2_DRAWN); +} + +/* ---- победа и выход --------------------------------------------------- */ + +/* Смерть Джафара не открывает дверь, а ставит флаг «выход разрешён». */ +TC_TEST(jaffar_death_opens_the_exit_flag) +{ + scene_level13(1); + /* charid у Джафара обычный: своим charid оригинал метит только скелета + * (enter_guard, seg002:196), а визиря отличают спрайты и события. */ + sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6); + Guard.sword = SWORD_2_DRAWN; + guardhp_curr = 0; /* последнее HP только что снято */ + pop_flash_time = 0; + pop_guard_tick(); + TC_EQ(pop_leveldoor_open, 2); + TC_EQ(pop_flash_time, JAFFAR_FLASH_TIME); + TC_EQ(pop_flash_color, POP_FLASH_WHITE); +} + +/* На других уровнях смерть стража флага не ставит (иначе дверь открывалась + * бы от любого убитого). */ +TC_TEST(jaffar_death_flag_is_level_gated) +{ + scene_level13(1); + pop_current_level = 12; + sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6); + Guard.sword = SWORD_2_DRAWN; + guardhp_curr = 0; + pop_guard_tick(); + TC_EQ(pop_leveldoor_open, 0); +} + +/* Уход ВЛЕВО при поднятом флаге жмёт кнопку комнаты 24. */ +TC_TEST(jaffar_exit_presses_button_in_room24) +{ + scene_level13(1); + tk_room_fg[JAFFAR_EXIT_ROOM][JAFFAR_EXIT_TILEPOS] = 15; /* opener */ + pop_leveldoor_open = 2; + tk_log_clear(); + pop_jaffar_exit(); + TC_TRUE(tk_log_count(TK_BUTTON) >= 1); +} + +TC_TEST(jaffar_exit_silent_before_victory) +{ + scene_level13(1); + tk_room_fg[JAFFAR_EXIT_ROOM][JAFFAR_EXIT_TILEPOS] = 15; + pop_leveldoor_open = 0; + tk_log_clear(); + pop_jaffar_exit(); + TC_EQ(tk_log_count(TK_BUTTON), 0); +} + +/* ---- три исключения loose-механики ----------------------------------- */ + +/* Сотрясение (приземление Кида) на 13-м уровне плиты НЕ трясёт: иначе оно + * сбило бы им отложенный старт, и гряда посыпалась бы разом. */ +TC_TEST(jaffar_knock_does_not_shake_tiles) +{ + scene_level13(1); + sc_room(room_loose, 1); + sc_kid_at(1, 2, 1); + Kid.curr_row = 2; + pop_loadkid(); + knock = -1; /* KNOCK_DOWN: ряд Кида */ + pop_check_knock(); + TC_EQ(pop_loose_modif[22], 0); + TC_EQ(pop_loose_modif[27], 0); +} + +/* А на прочих уровнях трясёт — контроль, что выключен именно 13-й. */ +TC_TEST(jaffar_knock_still_shakes_on_other_levels) +{ + scene_level13(1); + pop_current_level = 12; + sc_room(room_loose, 1); + sc_kid_at(1, 2, 1); + Kid.curr_row = 2; + pop_loadkid(); + knock = -1; + pop_check_knock(); + TC_EQ(pop_loose_modif[22], 0x80); +} + +/* Фаза с ОТРИЦАТЕЛЬНЫМ стартом на 13-м уровне не гасится, а досчитывает + * до провала: это и есть падающая гряда. Проверяем на плите ПОЛА (тот же + * код, что у плит-потолков, но без mob-обвязки, которая в набор не + * линкуется). */ +TC_TEST(jaffar_negative_phase_counts_through_to_fall) +{ + uint8_t i; + scene_level13(1); + sc_room(room_loose, 1); + sc_kid_at(1, 0, 1); + pop_loose_modif[22] = (uint8_t)-4; /* старт «через 4 кадра» */ + /* 4 кадра — на выход из отрицательной зоны, дальше 10 — обычный отсчёт; + * 30 с запасом. Ванильная ветка «тряска кончилась» обнулила бы фазу на + * первом же кадре, и плита осталась бы стоять. */ + for (i = 0; i < 30; i++) pop_loose_tick(); + TC_EQ(pop_tile_at(2, 2), 0); /* провалилась: тайл стал пустым */ +} + +/* На прочих уровнях та же фаза читается как тряска и гаснет — плита цела. */ +TC_TEST(jaffar_negative_phase_is_shake_on_other_levels) +{ + uint8_t i; + scene_level13(1); + pop_current_level = 12; + sc_room(room_loose, 1); + sc_kid_at(1, 0, 1); + pop_loose_modif[22] = (uint8_t)-4; + for (i = 0; i < 30; i++) pop_loose_tick(); + TC_EQ(pop_tile_at(2, 2), L); + TC_EQ(pop_loose_modif[22], 0); +} + +/* ---- check_fall_flo --------------------------------------------------- */ + +/* Вход в комнату 23: шести плитам ряда 2 комнаты СВЕРХУ раздаётся + * отрицательная фаза, остальным — ничего. */ +static void fall_flo_scene(uint8_t room) +{ + uint8_t i; + scene_level13(room); + for (i = 0; i < 10; i++) above_row[i] = room_loose[20 + i]; + pop_map_set_above(above_row); + pop_map_set_edges(0, 0, 1 /* up */, 0); + pop_map_set_above(above_row); + pop_loose_reset(); +} + +TC_TEST(jaffar_fall_flo_seeds_the_row_above) +{ + uint8_t c, seeded = 0; + fall_flo_scene(POP_LOOSE_TILES_ROOM1); + pop_check_fall_flo(); + for (c = 2; c <= 7; c++) + if ((int8_t)pop_ceil_modif[c] < 0) seeded++; + TC_EQ(seeded, 6); + /* Колонки вне диапазона 2..7 не тронуты (там в комнате пол, не loose). */ + TC_EQ(pop_ceil_modif[0], 0); + TC_EQ(pop_ceil_modif[9], 0); +} + +TC_TEST(jaffar_fall_flo_ignores_other_rooms) +{ + uint8_t c; + fall_flo_scene((uint8_t)(POP_LOOSE_TILES_ROOM1 + 1)); + pop_check_fall_flo(); + for (c = 0; c < 10; c++) TC_EQ(pop_ceil_modif[c], 0); +} + +TC_TEST(jaffar_fall_flo_is_level_gated) +{ + uint8_t c; + fall_flo_scene(POP_LOOSE_TILES_ROOM2); + pop_current_level = 12; + pop_check_fall_flo(); + for (c = 0; c < 10; c++) TC_EQ(pop_ceil_modif[c], 0); +} + +/* Повторный вход не перебивает уже идущий отсчёт (иначе гряда «зависала» + * бы, пока игрок бегает туда-сюда). */ +TC_TEST(jaffar_fall_flo_does_not_restart_running_countdown) +{ + fall_flo_scene(POP_LOOSE_TILES_ROOM2); + pop_ceil_modif[3] = 5; /* плита уже падает */ + pop_check_fall_flo(); + TC_EQ(pop_ceil_modif[3], 5); +} + +void main(void) +{ + sc_init(); + TC_RUN(jaffar_notice_timer_set_leaving_room3_right); + TC_RUN(jaffar_notice_not_given_after_victory); + TC_RUN(jaffar_notice_not_given_from_other_room); + TC_RUN(jaffar_notice_is_level_gated); + TC_RUN(jaffar_notice_timer_counts_down); + TC_RUN(jaffar_holds_still_while_notice_timer_runs); + TC_RUN(jaffar_draws_sword_after_notice_timer); + TC_RUN(jaffar_notice_timer_ignored_on_other_levels); + TC_RUN(jaffar_death_opens_the_exit_flag); + TC_RUN(jaffar_death_flag_is_level_gated); + TC_RUN(jaffar_exit_presses_button_in_room24); + TC_RUN(jaffar_exit_silent_before_victory); + TC_RUN(jaffar_knock_does_not_shake_tiles); + TC_RUN(jaffar_knock_still_shakes_on_other_levels); + TC_RUN(jaffar_negative_phase_counts_through_to_fall); + TC_RUN(jaffar_negative_phase_is_shake_on_other_levels); + TC_RUN(jaffar_fall_flo_seeds_the_row_above); + TC_RUN(jaffar_fall_flo_ignores_other_rooms); + TC_RUN(jaffar_fall_flo_is_level_gated); + TC_RUN(jaffar_fall_flo_does_not_restart_running_countdown); +} diff --git a/applications/PoP/roomtest/tests-host/t_phys.c b/applications/PoP/roomtest/tests-host/t_phys.c index cd45f40..bdb68cc 100644 --- a/applications/PoP/roomtest/tests-host/t_phys.c +++ b/applications/PoP/roomtest/tests-host/t_phys.c @@ -211,13 +211,44 @@ TC_TEST(phys_loose_floor_breaks) /* Журнал заглушек: плита обязана была отвалиться (кусок отрывается — * pop_loose_mob_spawn) и попросить перерисовку. Это уже не * характеризация, а содержательная проверка — заглушка-пустышка её не - * дала бы. Через trob loose у нас НЕ идёт (только пики/ворота), у него - * свой pop_loose_modif — поэтому спрашиваем именно про кусок. */ + * дала бы. Пока плита ЖИВЁТ в отрисованной комнате, фазой владеет + * pop_loose_modif, а не trob — поэтому спрашиваем именно про кусок. */ TC_TRUE(tk_log_count(TK_MOB_SPAWN) > 0); TC_TRUE(tk_log_count(TK_REDRAW) > 0); #endif } +TC_TEST(phys_loose_survives_room_change) +{ + /* GUARD против бага «плита не успевает провалиться»: Кид расшатал плиту + * и ушёл в соседнюю комнату — отсчёт обязан ПЕРЕЖИТЬ смену комнаты. + * В оригинале терять нечего (loose — обычный trob в общем списке, фаза + * в curr_room_modif); у нас фаза живёт в pop_loose_modif[] текущей + * комнаты, и её сдаёт/забирает пара leave_room + reset. */ + uint8_t pos = 1 * 10 + 4; /* loose-плита сцены room_loose */ + uint8_t phase = 3; /* середина отсчёта до провала (1..10) */ + + /* Саму анимацию проверяет phys_loose_floor_breaks выше; здесь нужна + * ровно передача фазы, поэтому ставим её явно — прогон бы привязал тест + * к тому, на каком кадре Кид добежит до плиты. */ + sc_room(room_loose, 1); + pop_loose_modif[pos] = phase; + + /* Уход из комнаты: фаза уезжает в room_modif + trob... */ + tk_log_clear(); + pop_loose_leave_room(); + TC_TRUE(tk_log_count(TK_TROB) > 0); /* плита сдана в общий список */ + + /* ...и возвращается при входе обратно — а не обнуляется, как раньше. */ + pop_loose_reset(); + TC_EQ(pop_loose_modif[pos], phase); + + /* Старт уровня, наоборот, обязан фазу ЗАБЫТЬ (load_level восстанавливает + * уровень целиком) — иначе дрожь протекала бы в следующую жизнь. */ + pop_loose_forget(); + TC_EQ(pop_loose_modif[pos], 0); +} + TC_TEST(phys_running_jump_over_3tile_gap) { /* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок. @@ -295,6 +326,7 @@ void main(void) TC_RUN(phys_jump_up_nothing_above); TC_RUN(phys_run_off_ledge); TC_RUN(phys_loose_floor_breaks); + TC_RUN(phys_loose_survives_room_change); TC_RUN(phys_running_jump_over_3tile_gap); TC_RUN(phys_feather_fall_is_slow_and_harmless); } diff --git a/applications/PoP/roomtest/tests-host/t_shadow.c b/applications/PoP/roomtest/tests-host/t_shadow.c new file mode 100644 index 0000000..2c96934 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_shadow.c @@ -0,0 +1,332 @@ +/* + * t_shadow — спецсобытия УРОВНЯ 12: встреча с тенью, общий урон, слияние. + * + * Что здесь проверяется и почему именно тестом, а не прогоном в MAME. Вся + * сцена уровня 12 — это набор УСЛОВИЙ, каждое из которых легко сломать, + * ничего не трогая в самой тени: тайл меча, знак `pop_united_shadow`, + * charid соперника, номер уровня. Проверить их в эмуляторе — значит + * дойти до 12-го уровня и разыграть бой; здесь это шесть вызовов. + * + * Порт: seg002:0064 check_shadow, seg002:0536 sword_disappears, + * seg006:2033 check_killed_shadow, seg000:1518 do_delta_hp, + * seg003:0735 timers, seg006:1063 «появление плит». + * + * Сам ИИ тени (autocontrol_shadow_level12) сюда НЕ вынесен: он решает + * «куда идти», и его исход — траектория, то есть предмет прогона со + * сценой и эталоном, а не одиночной проверки. Набор линкует guards.c + * (как t_mouse), поэтому добавить такой сценарий можно здесь же. + */ +#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" +#include "pop_tune.h" +#include "scene.h" +#include "stubs.h" + +#define E 0 /* empty */ +#define F 1 /* floor */ +#define W 20 /* wall */ +#define S 22 /* sword */ + +/* Комната 15 в объёме, который нужен событию: меч в (0,1), пол под ним. */ +static const uint8_t room15[30] = { + F, S, F, F, F, F, F, F, E, E, + W, W, W, W, W, W, W, W, F, F, + W, W, W, W, W, W, W, W, W, W, +}; + +/* Комната 2: верхний ряд с ПРОПАСТЬЮ в колонках 4..6 — те самые плиты, + * которые появляются под ногами после слияния. */ +static const uint8_t room2[30] = { + F, F, F, F, E, E, E, F, F, F, + W, W, W, W, W, W, W, W, W, W, + W, W, W, W, W, W, W, W, W, W, +}; + +/* Общая точка входа: уровень 12, слот соперника пуст, слияния не было. */ +static void scene_level12(void) +{ + uint8_t i; + pop_guard_reset(); + pop_current_level = SHADOW_FIGHT_LEVEL; + pop_united_shadow = 0; + pop_shadow_init = 0; + for (i = 0; i < 30; i++) tk_room_fg[SHADOW_FIGHT_ROOM][i] = room15[i]; + tk_log_clear(); +} + +/* ---- check_shadow: когда тень поднимается ---------------------------- */ + +/* Меч ещё лежит — тени нет. Условие ОБРАТНО уровню 5 (там тень приходит, + * пока зелье на месте), и перепутать их — самая вероятная ошибка порта. */ +TC_TEST(shadow12_waits_until_sword_taken) +{ + scene_level12(); + TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 0); + TC_EQ(Guard.charid, 0); +} + +/* Меч подобран — тень падает в комнату (init_shad_12 + seq_7_fall). */ +TC_TEST(shadow12_rises_when_sword_gone) +{ + scene_level12(); + tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS] = F; + TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 1); + TC_EQ(Guard.charid, CHARID_1_SHADOW); + TC_EQ(Guard.x, 0x51); + /* y = 232 — НИЖЕ поля (192), и это не ошибка данных: поле беззнаковое, + * тень «падает» из-под комнаты и появляется сверху. А curr_row при + * этом РОВНО ИЗ ТАБЛИЦЫ (0), а не пересчитанный из y (тот дал бы −1): + * do_init_shad копирует семь полей как есть и ряд не трогает. Обе + * величины закреплены тестом — «починить» их выглядит соблазнительно. */ + TC_EQ(Guard.y, 0xE8); + TC_EQ(Guard.curr_row, 0); + TC_EQ(Guard.direction, 0); /* dir_0_right */ + TC_EQ(Guard.room, SHADOW_FIGHT_ROOM); + TC_EQ(guardhp_curr, 4); +} + +/* После слияния тень не поднимается больше НИКОГДА (флаг ушёл в −1). */ +TC_TEST(shadow12_never_rises_after_uniting) +{ + scene_level12(); + tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS] = F; + pop_united_shadow = -1; + TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 0); + TC_EQ(Guard.charid, 0); +} + +/* В чужой комнате событие не срабатывает — там поднимается обычный страж + * (возврат 0 = «занимайся слотом сам»). */ +TC_TEST(shadow12_other_room_leaves_slot_alone) +{ + scene_level12(); + TC_EQ(pop_check_shadow((uint8_t)(SHADOW_FIGHT_ROOM - 1)), 0); + TC_EQ(Guard.charid, 0); +} + +/* ---- sword_disappears ------------------------------------------------ */ + +TC_TEST(shadow12_sword_vanishes_leaving_room18) +{ + scene_level12(); + Kid.room = SWORD_GONE_ROOM; + tk_modif[SHADOW_SWORD_TILEPOS] = 5; /* блеск меча */ + pop_sword_disappears(); + TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], F); + /* Модификатор гасится вместе с тайлом: ненулевой modif рисует на полу + * «фальшивый» узор (та же причина, что у поднятого зелья). */ + TC_EQ(tk_modif[SHADOW_SWORD_TILEPOS], 0); +} + +TC_TEST(shadow12_sword_stays_leaving_other_room) +{ + scene_level12(); + Kid.room = (uint8_t)(SWORD_GONE_ROOM - 1); + pop_sword_disappears(); + TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], S); +} + +/* На других уровнях комната 18 — обычная комната. */ +TC_TEST(shadow12_sword_event_is_level_gated) +{ + scene_level12(); + pop_current_level = 11; + Kid.room = SWORD_GONE_ROOM; + pop_sword_disappears(); + TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], S); +} + +/* ---- таймер вспышки -------------------------------------------------- */ + +/* Счётчик досчитывает до нуля и уходит в −1 — «уже слились». Ноль как + * конечное состояние был бы неотличим от «слияния не было». */ +TC_TEST(shadow12_timer_ends_at_minus_one) +{ + pop_united_shadow = 3; + pop_frame_timers(); TC_EQ(pop_united_shadow, 2); + pop_frame_timers(); TC_EQ(pop_united_shadow, 1); + pop_frame_timers(); TC_EQ(pop_united_shadow, -1); + pop_frame_timers(); TC_EQ(pop_united_shadow, -1); /* и больше не меняется */ +} + +TC_TEST(shadow12_timer_idle_when_zero) +{ + pop_united_shadow = 0; + pop_frame_timers(); + TC_EQ(pop_united_shadow, 0); +} + +/* ---- общий урон (do_delta_hp) ---------------------------------------- */ + +static void hp_scene(uint8_t charid, uint8_t level) +{ + scene_level12(); + pop_current_level = level; + Guard.charid = charid; + guardhp_curr = guardhp_max = 4; + hitp_beg_lev = 3; + pop_kid_hp_reset(); + guardhp_delta = -1; + hitp_delta = 0; +} + +/* Ранил тень — ранил себя: урон КОПИРУЕТСЯ, а не делится. */ +TC_TEST(shadow12_damage_is_shared) +{ + hp_scene(CHARID_1_SHADOW, SHADOW_FIGHT_LEVEL); + pop_do_delta_hp(); + TC_EQ(guardhp_curr, 3); + TC_EQ(hitp_curr, 2); +} + +TC_TEST(shadow12_damage_not_shared_with_guard) +{ + hp_scene(CHARID_2_GUARD, SHADOW_FIGHT_LEVEL); + pop_do_delta_hp(); + TC_EQ(guardhp_curr, 3); + TC_EQ(hitp_curr, 3); +} + +/* Тень уровней 4/5/6 боевой не бывает, но проверка обязана быть по УРОВНЮ, + * а не только по charid: иначе безобидная тень уровня 5 делилась бы HP. */ +TC_TEST(shadow12_damage_not_shared_on_other_level) +{ + hp_scene(CHARID_1_SHADOW, 5); + pop_do_delta_hp(); + TC_EQ(guardhp_curr, 3); + TC_EQ(hitp_curr, 3); +} + +/* ---- check_killed_shadow --------------------------------------------- */ + +static void kill_scene(uint8_t charid, int8_t kid_alive, int8_t guard_alive) +{ + scene_level12(); + Guard.charid = charid; + Guard.alive = guard_alive; + guardhp_curr = guardhp_max = 4; + hitp_beg_lev = 3; + pop_kid_hp_reset(); + Kid.charid = CHARID_0_KID; + Kid.alive = kid_alive; + guardhp_delta = hitp_delta = 0; + pop_flash_time = 0; + pop_immortal = 0; +} + +/* Тень мертва — Киду отсчитывается 100 урона, то есть гарантированно всё. */ +TC_TEST(shadow12_killing_shadow_kills_kid) +{ + kill_scene(CHARID_1_SHADOW, -1, 0); + pop_check_killed_shadow(); + TC_EQ(hitp_delta, -3); + TC_TRUE(pop_flash_time != 0); + TC_EQ(pop_flash_color, POP_FLASH_WHITE); +} + +/* И симметрично: смерть Кида добивает тень. */ +TC_TEST(shadow12_killing_kid_kills_shadow) +{ + kill_scene(CHARID_1_SHADOW, 0, -1); + pop_check_killed_shadow(); + TC_EQ(guardhp_delta, -4); +} + +/* Со стражем связи нет — условие оригинала `(charid | charid) == shadow` + * проходит только для пары Кид/тень. */ +TC_TEST(shadow12_kill_link_ignores_guard) +{ + kill_scene(CHARID_2_GUARD, -1, 0); + pop_check_killed_shadow(); + TC_EQ(hitp_delta, 0); + TC_EQ(pop_flash_time, 0); +} + +/* Оба живы — событие молчит (иначе бой убивал бы обоих в первом же кадре). */ +TC_TEST(shadow12_kill_link_silent_while_both_alive) +{ + kill_scene(CHARID_1_SHADOW, -1, -1); + pop_check_killed_shadow(); + TC_EQ(hitp_delta, 0); + TC_EQ(guardhp_delta, 0); +} + +/* ---- «появление плит» ------------------------------------------------ */ + +/* Сцена: Кид стоит в комнате 2 на колонке 4 верхнего ряда — прямо над + * пропастью. До слияния он обязан провалиться, после — достроить пол. */ +static void floors_scene(void) +{ + uint8_t i; + scene_level12(); + for (i = 0; i < 30; i++) tk_room_fg[2][i] = room2[i]; + sc_room(room2, 2); + sc_kid_at(4, 0, 1); + sc_trace_clear(); +} + +TC_TEST(shadow12_floors_appear_after_uniting) +{ + floors_scene(); + pop_united_shadow = -1; + sc_run(0, 2); + /* Не упал: действие осталось стоечным, ряд тот же. */ + TC_EQ(Kid.curr_row, 0); + TC_TRUE(Kid.action != 4); /* actions_4_in_freefall */ + /* Плита появилась И в карте комнаты, И в данных уровня. */ + TC_EQ(pop_tile_at(4, 0), 1); + TC_EQ(tk_room_fg[2][4], 1); + /* Перерисованы две клетки — сама и соседняя справа, на обеих страницах. */ + TC_TRUE(tk_log_count(TK_REDRAW) >= 2); +} + +/* До слияния пропасть остаётся пропастью. */ +TC_TEST(shadow12_floors_hidden_before_uniting) +{ + floors_scene(); + pop_united_shadow = 0; + sc_run(0, 2); + TC_EQ(pop_tile_at(4, 0), 0); + TC_TRUE(Kid.curr_row != 0 || Kid.action == 4); +} + +/* Событие привязано к КОМНАТЕ: в комнате 3 того же уровня пола нет. */ +TC_TEST(shadow12_floors_only_in_their_rooms) +{ + floors_scene(); + pop_united_shadow = -1; + Kid.room = 3; + sc_run(0, 2); + TC_EQ(pop_tile_at(4, 0), 0); +} + +void main(void) +{ + sc_init(); + TC_RUN(shadow12_waits_until_sword_taken); + TC_RUN(shadow12_rises_when_sword_gone); + TC_RUN(shadow12_never_rises_after_uniting); + TC_RUN(shadow12_other_room_leaves_slot_alone); + TC_RUN(shadow12_sword_vanishes_leaving_room18); + TC_RUN(shadow12_sword_stays_leaving_other_room); + TC_RUN(shadow12_sword_event_is_level_gated); + TC_RUN(shadow12_timer_ends_at_minus_one); + TC_RUN(shadow12_timer_idle_when_zero); + TC_RUN(shadow12_damage_is_shared); + TC_RUN(shadow12_damage_not_shared_with_guard); + TC_RUN(shadow12_damage_not_shared_on_other_level); + TC_RUN(shadow12_killing_shadow_kills_kid); + TC_RUN(shadow12_killing_kid_kills_shadow); + TC_RUN(shadow12_kill_link_ignores_guard); + TC_RUN(shadow12_kill_link_silent_while_both_alive); + TC_RUN(shadow12_floors_appear_after_uniting); + TC_RUN(shadow12_floors_hidden_before_uniting); + TC_RUN(shadow12_floors_only_in_their_rooms); +} diff --git a/applications/PoP/toolchain/pop_pack_guard.py b/applications/PoP/toolchain/pop_pack_guard.py index 89215ff..8598f9e 100644 --- a/applications/PoP/toolchain/pop_pack_guard.py +++ b/applications/PoP/toolchain/pop_pack_guard.py @@ -27,8 +27,9 @@ from pop_pack_kid import transpose_cols, vflip_page # остальных наборов curr_guard_color = 0 (seg002:186) — значит цвет берётся # из ФАЙЛА набора (res750.pal), и подменять его не надо. SETS = { - "GUARD": ("GUARD", "guard", None), - "SKEL": ("SKEL", "skel", "res750.pal"), + "GUARD": ("GUARD", "guard", None), + "SKEL": ("SKEL", "skel", "res750.pal"), + "VIZIER": ("VIZIER", "vizier", "res750.pal"), } SET_NAME = "GUARD" GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD") @@ -42,7 +43,8 @@ MAX_ID = 33 # атлас-индексы 0..33 = res751..res784 def select_set(name): - """Переключить упаковщик на набор (GUARD/SKEL): каталоги и число кадров.""" + """Переключить упаковщик на набор (GUARD/SKEL/VIZIER): каталоги и число + кадров.""" global SET_NAME, GUARD_DIR, OUT_DIR, MAX_ID if name not in SETS: B.die("неизвестный набор %s (есть: %s)" % (name, ", ".join(SETS))) @@ -142,19 +144,23 @@ def write_pal_header(out_path): "};", "", ] - # Палитра скелета — из ЕГО набора (res750.pal), а не из res10.bin: на - # уровне 3 curr_guard_color = 0, оригинал палитру не подменяет вовсе. + # Палитры наборов со СВОИМ цветом (скелет, визирь) — из их res750.pal, а + # не из res10.bin: на таких уровнях curr_guard_color = 0 (seg002:186), и + # оригинал палитру не подменяет вовсе. saved = SET_NAME - select_set("SKEL") - skel = set_palette() - select_set(saved) - if skel: + for name, cname, human in (("SKEL", "pop_skel_pal", "Скелет"), + ("VIZIER", "pop_vizier_pal", "Джафар")): + select_set(name) + pal = set_palette() + select_set(saved) + if not pal: + continue vals = [] - for (r, g, b) in skel: + for (r, g, b) in pal: vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0] lines += [ - "/* Скелет (SKEL/res750.pal): свой набор — свой цвет. */", - "static const uint8_t pop_skel_pal[64] = {", + "/* %s (%s/res750.pal): свой набор — свой цвет. */" % (human, name), + "static const uint8_t %s[64] = {" % cname, " " + ",".join("0x%02X" % v for v in vals) + ",", "};", "",