Уровни 12 и 13: тень, Джафар, падающие плиты + разбор слоёв

Уровень 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 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 18:21:52 +03:00
parent 716f6f9273
commit 67d62e3e8b
37 changed files with 3169 additions and 79 deletions
+1
View File
@@ -11,6 +11,7 @@
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему | | [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему |
| [`perf_backlog.md`](perf_backlog.md) | **Отложенная оптимизация отрисовки** с замерами + как мерить (wait-state'ы, границы кадра) | | [`perf_backlog.md`](perf_backlog.md) | **Отложенная оптимизация отрисовки** с замерами + как мерить (wait-state'ы, границы кадра) |
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | | [`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) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки | | [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди | | [`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 | | [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии из `BUGS_CLOSED.md`, дифф против SDLPoP |
+159
View File
@@ -245,3 +245,162 @@ offscreen неперевёрнутым, а зеркалит только при
**Что проверять при регрессе.** Уровень 9: выпить зелёное зелье — картинка **Что проверять при регрессе.** Уровень 9: выпить зелёное зелье — картинка
переворачивается ровно один раз, лишних языков пламени по комнате нет. Чит переворачивается ровно один раз, лишних языков пламени по комнате нет. Чит
U даёт тот же результат (он идёт тем же путём). 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`);
* стартовое значение берётся на единицу «отрицательнее» (`n1`).
**Чем платим.** Ничем в наблюдаемом поведении: суммарная задержка остаётся
`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` на Джафаре → белая вспышка,
уход ВЛЕВО открывает дверь уровня. Честная победа в бою даёт то же самое.
+462
View File
@@ -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, &nbsp;&nbsp; 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`.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+349 -2
View File
@@ -15,7 +15,7 @@
| ID | что | тип | статус | | ID | что | тип | статус |
|----|-----|-----|--------| |----|-----|-----|--------|
| [SPRITE-ZERO-W0](#sprite-zero-w0) | кадр персонажа изредка отдаёт спрайт 0x0 (залипание уже вылечено) | **редкий** | открыт: нужна трасса маппинга W0 | | [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) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить | | [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить |
| [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт | | [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт |
| [TORCH-ANIM-RIGHT](#torch-anim-right) | под запечённым пламенем застывают не только челюсти чомпера | **низкий** | открыт: на уровнях 1-4 такого соседства нет | | [TORCH-ANIM-RIGHT](#torch-anim-right) | под запечённым пламенем застывают не только челюсти чомпера | **низкий** | открыт: на уровнях 1-4 такого соседства нет |
@@ -23,6 +23,13 @@
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается | | [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз | | [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику | | [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. Симптом редкий (пользователь поймал дважды). читать её из MAME. Симптом редкий (пользователь поймал дважды).
<a id="gate-fore-kid"></a> <a id="gate-fore-kid"></a>
## 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, верхние ворота): Нашёл пользователь 2026-08-11 (уровень 5, комната 24, верхние ворота):
Кид стоит на тайле ворот, а решётка рисуется ПОД ним — он виден целиком, Кид стоит на тайле ворот, а решётка рисуется ПОД ним — он виден целиком,
@@ -465,3 +496,319 @@ if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
**Проверять:** уровень 5, комната 24 — встать в проём верхних ворот **Проверять:** уровень 5, комната 24 — встать в проём верхних ворот
(колонка 1 ряда 0) при частично поднятой решётке; прутья должны (колонка 1 ряда 0) при частично поднятой решётке; прутья должны
перекрывать Кида. Тот же случай — любые ворота на уровнях 1-3. перекрывать Кида. Тот же случай — любые ворота на уровнях 1-3.
---
<a id="guard-entry-death"></a>
## 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`, а не умереть;
падение на стража сверху по-прежнему безболезненно (это оригинал, не баг).
---
<a id="loose-room-change"></a>
## 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)), плита-потолок, рестарт уровня после смерти
на дрожащей плите.
---
<a id="loose-seam-anim"></a>
## 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; в оригинале плита в шве дрожит и
роняет кусок так же, как в своей комнате.
---
<a id="jump-floor-legs"></a>
## 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; за передней
колонной Кид уходит и сейчас — это не должно сломаться.
---
<a id="mob-clip-right"></a>
## 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` на кусок за кадр —
при шести падающих плитах это шесть отрисовок тайла.
---
<a id="mid-overlay-layer"></a>
## 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) — там та же техническая
сердцевина: привести наши слои в соответствие с таблицами оригинала.
---
<a id="guard-fallout-victory"></a>
## 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`
живёт в резиденте.
+13 -1
View File
@@ -94,8 +94,16 @@ SKEL_DIR := $(CURDIR)/../poc/res/skel
# страж (tbl_guard_type[9] = 0). См. pop_pack_guard.py. # страж (tbl_guard_type[9] = 0). См. pop_pack_guard.py.
SKEL_DATA := $(SKEL_DIR)/g0.atl $(SKEL_DIR)/g1.atl $(SKEL_DIR)/g2.atl \ SKEL_DATA := $(SKEL_DIR)/g0.atl $(SKEL_DIR)/g1.atl $(SKEL_DIR)/g2.atl \
$(SKEL_DIR)/g3.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 include $(PROJ_ROOT)/app.mk
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS), # Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
@@ -111,6 +119,7 @@ hdd: $(EXAMPLE).exe
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \
$(foreach f,$(SKEL_DATA),SKEL:$(f)) \ $(foreach f,$(SKEL_DATA),SKEL:$(f)) \
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
$(foreach f,$(LVL_DATA),LEVELS:$(f)) $(foreach f,$(LVL_DATA),LEVELS:$(f))
TC := $(PROJ_ROOT)/applications/PoP/toolchain TC := $(PROJ_ROOT)/applications/PoP/toolchain
@@ -122,6 +131,9 @@ $(GUARD_DATA): $(TC)/pop_pack_guard.py
$(SKEL_DATA): $(TC)/pop_pack_guard.py $(SKEL_DATA): $(TC)/pop_pack_guard.py
cd $(TC) && python3 pop_pack_guard.py SKEL 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 $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
cd $(TC) && python3 pop_pack_kid.py cd $(TC) && python3 pop_pack_kid.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
+145
View File
@@ -231,6 +231,151 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
## P0 — делаем сейчас ## P0 — делаем сейчас
### <a id="l12-shadow"></a>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).
<a id="bg-once"></a>
### 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) и убедиться, что ни один фоновый элемент не
перерисовывается в кадре, где его тайл не менялся; замерить кадр до и после.
<a id="l12-shadow-spr"></a>
### 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`]) —
только правильный набор спрайтов вместо чужого.
<a id="l12-seam-polish"></a>
### L12-SEAM-POLISH. Переход 12 -> 13 выглядит коряво
Механика работает (комната 23, без заставки и без сброса HP), но сама
анимация прохода рваная — замечено пользователем 2026-08-13. Полишинг,
берётся вместе с остальной шлифовкой переходов.
<a id="l13-jaffar"></a>
### 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 НЕ сбрасывается.
### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1) ### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1)
> **Статус 2026-08-12: код написан, ждёт живой проверки в MAME.** Сделаны все > **Статус 2026-08-12: код написан, ждёт живой проверки в MAME.** Сделаны все
+216 -10
View File
@@ -29,6 +29,7 @@
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */ #define TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */ #define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
#define TILE_FLOOR 1
#define TILE_SPIKE 2 #define TILE_SPIKE 2
#define TILE_GATE 4 #define TILE_GATE 4
#define TILE_DOORTOP_FLOOR 7 #define TILE_DOORTOP_FLOOR 7
@@ -270,18 +271,28 @@ static uint8_t demo_index; /* текущая запись
#define SHADOW_SKILL 3 #define SHADOW_SKILL 3
#define SHADOW_HP 4 #define SHADOW_HP 4
#define SEQ_2_STAND 2 #define SEQ_2_STAND 2
#define SEQ_7_FALL 7
#define TILE_POTION 10 #define TILE_POTION 10
#define TILE_SWORD 22
/* Таблицы init_shad_N (data:0EEA/0EF2) — ПЕРВЫЕ СЕМЬ полей Char в том же /* Таблицы init_shad_N (data:0EEA/0EF2) — ПЕРВЫЕ СЕМЬ полей Char в том же
* порядке, в каком их копирует оригинал (`memcpy(&Char, source, 7)`): * порядке, в каком их копирует оригинал (`memcpy(&Char, source, 7)`):
* frame, x, y, direction, curr_col, curr_row, action. * frame, x, y, direction, curr_col, curr_row, action.
* 5: x 55, y 55, вправо, колонка 1 (ещё за левым краем экрана), ряд 0; * 5: x 55, y 55, вправо, колонка 1 (ещё за левым краем экрана), ряд 0;
* 6: x 81, y 118, вправо, колонка 0, ряд 1 тень стоит на границе * 6: x 81, y 118, вправо, колонка 0, ряд 1 тень стоит на границе
* (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту. */ * (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_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_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(); pop_loadshad();
Char.frame = tbl[0]; 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.alive = Kid.alive; /* наследование от Кида, см. шапку */
Char.sword = Kid.sword; Char.sword = Kid.sword;
Char.fall_x = Char.fall_y = 0; Char.fall_x = Char.fall_y = 0;
pop_char_set_seq(SEQ_2_STAND); pop_char_set_seq(seq);
pop_saveshad(); pop_saveshad();
demo_time = 0; 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 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): условий на содержимое комнаты нет вовсе — /* Уровень 6 (seg002:0090): условий на содержимое комнаты нет вовсе —
* тень встаёт при КАЖДОМ входе в комнату 1. Оригинал здесь ещё играет * тень встаёт при КАЖДОМ входе в комнату 1. Оригинал здесь ещё играет
* sound_25_presentation один раз, отмечая это `leveldoor_open = 0x4D` * sound_25_presentation один раз, отмечая это `leveldoor_open = 0x4D`
@@ -316,7 +346,7 @@ uint8_t pop_check_shadow(uint8_t room) __banked
* переменную портить незачем TODO вместе со звуком. */ * переменную портить незачем TODO вместе со звуком. */
if (pop_current_level == SHADOW_STEP_LEVEL) { if (pop_current_level == SHADOW_STEP_LEVEL) {
if (room != SHADOW_STEP_ROOM) return 0; 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; return 1;
} }
if (pop_current_level != SHADOW_STEAL_LEVEL) return 0; if (pop_current_level != SHADOW_STEAL_LEVEL) return 0;
@@ -324,10 +354,108 @@ uint8_t pop_check_shadow(uint8_t room) __banked
/* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража /* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража
* комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */ * комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */
if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1; if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1;
do_init_shad(init_shad_5, room); do_init_shad(init_shad_5, room, SEQ_2_STAND);
return 1; 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..) --------------------------- */ /* ---- Синтетический ввод (seg002:0706..) --------------------------- */
static void move_0_nothing(void) static void move_0_nothing(void)
{ {
@@ -399,7 +527,10 @@ static void autocontrol_guard_inactive(void)
} }
} }
if (can_guard_see_kid) { if (can_guard_see_kid) {
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */ /* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч).
* Уровень 13: пока идёт фора после встречи (pop_meet_jaffar), Джафар
* стоит и ждёт это его единственное отличие от прочих стражей. */
if (pop_current_level != JAFFAR_LEVEL || pop_guard_notice_timer == 0)
move_down_forw(); move_down_forw();
} }
} }
@@ -686,6 +817,82 @@ static void autocontrol_shadow_level6(void)
move_1_forward(); 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: /* autocontrol_mouse (seg002:07EB). Весь «ИИ» мыши — два порога по X:
* добежала влево до POP_MOUSE_TURN_X seq_107 (встать, поднять голову, * добежала влево до POP_MOUSE_TURN_X seq_107 (встать, поднять голову,
* развернуться и уйти), вернулась вправо до POP_MOUSE_LEAVE_X исчезнуть. * развернуться и уйти), вернулась вправо до 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) static void autocontrol_shadow(void)
{ {
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4(); if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5(); if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5();
if (pop_current_level == SHADOW_STEP_LEVEL) autocontrol_shadow_level6(); 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). */ /* 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.charid == CHARID_24_MOUSE) { autocontrol_opponent(); return; }
if (Char.alive < 0 && guardhp_curr == 0) { if (Char.alive < 0 && guardhp_curr == 0) {
Char.alive = 0; Char.alive = 0;
/* on_guard_killed (seg006:1927) — у нас пока только звук победы, on_guard_killed(); /* уровень 13: вспышка + «выход открыт» */
* которого нет; для уровня 1 других эффектов там нет. */
} }
autocontrol_opponent(); autocontrol_opponent();
pop_control(); pop_control();
+71 -3
View File
@@ -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); 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 (code == 20) { /* draw_tile_bottom: стена */
if (!pop_palace) /* seg008:576 */ if (!pop_palace) /* seg008:576 */
pop_wall_b(POP_WALL_FRAM_BOTTOM[pop_wall_modifier(-1, col)], x, dby); 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) { } else if (t->bottom_id) {
pop_env_b(t->bottom_id, x, dby); 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 ещё */ /* на 141 — до него Kid ещё */
/* в ряду 1 ПОД полом [0,3]) */ /* в ряду 1 ПОД полом [0,3]) */
if (do_ov) { if (do_ov) {
/* ВЕСЬ диапазон рядов, а не только два крайних. В оригинале
* (redraw_at_char2, seg003:0645) стоит ровно два вызова
* char_bottom_row и char_top_row, и этого хватает, потому что
* там они ВСЕГДА соседние. У нас между ними бывает пропуск:
* char_footprint форсит rT = rB1 (голова торчит в ряд выше), а
* потом окно 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--) { for (c = cR; c >= cL; c--) {
if (action != 2) other_overlay_tile(rB, c); /* опорный ряд */ int8_t r2;
if (rT != rB) other_overlay_tile(rT, c); /* верхний ряд */ 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 (r = rT; r <= rB; r++)
for (c = cL; c <= cR; c++) for (c = cL; c <= cR; c++)
fore_tile(r, 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 = /* redraw_needed_above (seg008:0221): полоса кладки У ПОТОЛКА (ряд −1 =
* ряд 2 комнаты сверху). В оригинале она рисуется в самом конце обхода * ряд 2 комнаты сверху). В оригинале она рисуется в самом конце обхода
* тайлов ПОСЛЕ объектов, т.е. поверх персонажа (redraw_frames_above, * тайлов ПОСЛЕ объектов, т.е. поверх персонажа (redraw_frames_above,
+8
View File
@@ -160,6 +160,11 @@ void pop_gate_redraw(int row, int col) __banked;
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick * (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col) __banked; 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_tick(void) __banked;
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */ 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, /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после отрисовки персонажей, перед fore-слоем. */ * seg008:1561). Звать после отрисовки персонажей, перед fore-слоем. */
/* Отрисовка падающих кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed()
* фон кадра обязан быть готов до любого спрайта. */
void pop_loose_mob_draw(void) __banked;
void pop_loose_mob_draw_over(void) __banked; void pop_loose_mob_draw_over(void) __banked;
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */ /* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
+10 -3
View File
@@ -83,11 +83,17 @@ int pop_guard_load(void) __banked
"SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl", "SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl",
"SKEL\\g3.atl", 0 "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; const char *const *names;
int8_t type = pop_level_guard_type(pop_current_level); int8_t type = pop_level_guard_type(pop_current_level);
uint8_t i; uint8_t i;
if (type < 0) return 0; /* соперников на уровне нет */ 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++) { for (i = 0; i < GUARD_PAGES; i++) {
if (names[i] == 0) break; if (names[i] == 0) break;
if (atlas_load(&gp[i], names[i]) != 0) return -1; 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 при * из res10.bin по guards_color это делает pop_guard_set_palette при
* входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ * входе в комнату (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; 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(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
} }
+39 -4
View File
@@ -24,6 +24,9 @@ int8_t is_guard_notice;
uint8_t guard_skill; uint8_t guard_skill;
int8_t guard_refrac, justblocked, kid_sword_strike; int8_t guard_refrac, justblocked, kid_sword_strike;
int8_t offguard, holding_sword; 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 (Guard.charid == CHARID_4_SKELETON) return;
if (!pop_guard_present()) return; if (!pop_guard_present()) return;
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит /* HP не обнуляем — ставим дельту, её применит pop_do_delta_hp. Дальше
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше * play_guard увидит «жив, но HP ноль», сам переведёт в alive = 0 и
* общий control() ставит ему seq_71_dying. */ * вызовет 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; guardhp_delta = (int8_t)-guardhp_curr;
Guard.alive = 0;
} }
/* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */ /* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */
@@ -83,11 +95,34 @@ uint8_t pop_take_hp(uint8_t count)
return 0; 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 и обнулить /* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */ * её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
void pop_do_delta_hp(void) void pop_do_delta_hp(void)
{ {
int16_t hp; 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) { if (hitp_delta) {
hp = (int16_t)hitp_curr + hitp_delta; hp = (int16_t)hitp_curr + hitp_delta;
if (hp < 0) hp = 0; if (hp < 0) hp = 0;
+81 -1
View File
@@ -66,6 +66,9 @@
#define CHARID_0_KID 0 #define CHARID_0_KID 0
#define CHARID_1_SHADOW 1 #define CHARID_1_SHADOW 1
#define CHARID_2_GUARD 2 #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_4_SKELETON 4
#define CHARID_5_PRINCESS 5 #define CHARID_5_PRINCESS 5
/* МЫШЬ (спецсобытие уровня 8) — тоже персонаж слота Guard: своей таблицы /* МЫШЬ (спецсобытие уровня 8) — тоже персонаж слота Guard: своей таблицы
@@ -129,8 +132,41 @@
/* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех /* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех
* остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см. * остальных (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_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 записей). */ /* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
#define NUM_GUARD_SKILLS 12 #define NUM_GUARD_SKILLS 12
@@ -184,6 +220,23 @@ extern int8_t guard_refrac, justblocked, kid_sword_strike;
* holding_sword клинок в руке (для индикатора HP стража). */ * holding_sword клинок в руке (для индикатора HP стража). */
extern int8_t offguard, holding_sword; 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). */ /* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
extern pop_rnd_t pop_fight_seed; 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; 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. */ * кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
void pop_check_sword_hurting(void) __banked; void pop_check_sword_hurting(void) __banked;
+9 -2
View File
@@ -102,8 +102,15 @@ void pop_guard_enter(uint8_t room)
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return; if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
/* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты /* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты
* палитры общие на экран, и смена посреди кадра дала бы стража в новой * палитры общие на экран, и смена посреди кадра дала бы стража в новой
* палитре с полосой HP в старой. */ * палитре с полосой HP в старой.
pop_guard_set_palette(color); *
* Цвет из данных комнаты берётся ТОЛЬКО у обычного стража (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 считается из /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
* колонки тайла, а не берётся из файла. */ * колонки тайла, а не берётся из файла. */
Guard.room = room; Guard.room = room;
@@ -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, 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 #endif
+336 -16
View File
@@ -236,6 +236,16 @@ void pop_kid_hp_reset(void) __banked
pop_kid_dead = 0; 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. */ /* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */
void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; } 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); 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): физика работает /* Урон — через общий pop_take_hp (резидент pop_guard.c): физика работает
* над Char, значит и HP обязаны убавляться у ТОГО, кто сейчас в Char. Своя * над Char, значит и HP обязаны убавляться у ТОГО, кто сейчас в Char. Своя
* копия take_hp здесь была Kid-only и правила hitp_curr мимо дельты. */ * копия 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; 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; g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
/* fg соседей — прямо из уровня, одной скобкой доступа на обе комнаты. /* fg соседей — прямо из уровня, одной скобкой доступа на обе комнаты.
* Держать ещё одну копию в приложении незачем: карта единственный её * Держать ещё одну копию в приложении незачем: карта единственный её
* потребитель (отрисовке швов хватает кромочных колонок pop_room_load). */ * потребитель (отрисовке швов хватает кромочных колонок pop_room_load). */
if (left || right) { pop_map_refresh_edges();
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();
}
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия /* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и * недействительны (в оригинале 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) static void check_on_floor(void)
{ {
if (kid_cur_flags() & FRAME_NEEDS_FLOOR) { if (kid_cur_flags() & FRAME_NEEDS_FLOOR) {
if (get_tile_at_char() == TILE_WALL) if (get_tile_at_char() == TILE_WALL)
in_wall(); /* забежал в стену -> выровнять */ in_wall(); /* забежал в стену -> выровнять */
if (!tile_is_floor(g_curr_tile)) if (!tile_is_floor(g_curr_tile) && !floors_appear())
start_fall(); start_fall();
} }
} }
@@ -1466,6 +1529,40 @@ static void bumped_floor(void)
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */ 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) -------------------------------- * /* ---- check_collisions (seg004:0004) -------------------------------- *
* ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка * ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка
* удара выбирается обходом 90 по переходу флага 01. * удара выбирается обходом 90 по переходу флага 01.
@@ -2112,6 +2209,12 @@ static void check_press(void)
static void do_knock(int tile_row) static void do_knock(int tile_row)
{ {
int col, pos; 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 (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */
if (!g_above || !g_link_u) return; if (!g_above || !g_link_u) return;
for (col = 0; col < 10; col++) 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 /* Сброс состояния 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 void pop_loose_reset(void) __banked
{ {
uint8_t *mod;
int i; int i;
mod = g_room ? pop_trob_modif(g_room) : 0;
for (i = 0; i < 30; i++) { 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++) { 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_ceil_fell = 0;
pop_debris_at = 0; pop_debris_at = 0;
@@ -2158,6 +2363,82 @@ void pop_loose_reset(void) __banked
pop_loose_mob_room_changed(); 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), а во-вторых, стартовое значение берётся на
* единицу «отрицательнее» (n1): вместе это даёт ровно ту же суммарную
* задержку 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): плита прилетела Киду по голове. В беге /* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
* (кадры 5..14) не бьёт (кроме уровня 13 его тут нет); в висе/подъёме тоже * (кадры 5..14) не бьёт (кроме уровня 13 его тут нет); в висе/подъёме тоже
* (action >= hang_climb, кроме turn). Иначе 1 HP: смерть seq_22 (crushed), * (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) static void fell_on_your_head(void)
{ {
uint8_t frame = Char.frame, action = Char.action; 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)) { (action < ACT_HANGCLIMB || action == ACT_TURN)) {
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
if (pop_take_hp(1)) { /* «брызги» даст pop_do_delta_hp */ 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++) { for (pos = 0; pos < 30; pos++) {
if (pop_loose_modif[pos] != 0) { if (pop_loose_modif[pos] != 0) {
uint8_t m = ++pop_loose_modif[pos]; 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 & 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; pop_loose_modif[pos] = 0;
/* modif уже 0 → кадр покоя обоих тайлов (col и col+1: /* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
* правая грань loose живёт в соседе). ОБЕ страницы * правая грань loose живёт в соседе). ОБЕ страницы
@@ -2231,6 +2529,13 @@ void pop_loose_tick(void) __banked
} else { } else {
pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */ 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 — падение */ } else if (m >= 11) { /* loose_floor_delay — падение */
/* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!). /* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
* Debris появляется ТОЛЬКО там, где приземлился падающий * Debris появляется ТОЛЬКО там, где приземлился падающий
@@ -2244,6 +2549,11 @@ void pop_loose_tick(void) __banked
pop_trob_modif(g_room)[pos] = pop_palace; pop_trob_modif(g_room)[pos] = pop_palace;
pop_loose_modif[pos] = 0; pop_loose_modif[pos] = 0;
pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2); /* запечь пусто, обе стр. */ 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_mob_spawn(row, col); /* отрыв падающего куска */
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */ pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
} else { /* отсчёт 1..10 — дрожащий кадр */ } else { /* отсчёт 1..10 — дрожащий кадр */
@@ -2258,10 +2568,19 @@ void pop_loose_tick(void) __banked
for (col = 0; col < 10; col++) { for (col = 0; col < 10; col++) {
if (pop_ceil_modif[col] != 0) { if (pop_ceil_modif[col] != 0) {
uint8_t m = ++pop_ceil_modif[col]; 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 & 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); } pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
else pop_set_redraw_above(col, POP_RDA_CEIL, 1); 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 — плита рушится */ } else if (m >= 11) { /* loose_floor_delay — плита рушится */
pop_ceil_modif[col] = 0; pop_ceil_modif[col] = 0;
if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */ if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */
@@ -2629,6 +2948,7 @@ static void kid_phys(void)
fall_accel(); fall_accel();
fall_speed(); fall_speed();
determine_col(); determine_col();
bump_into_opponent(); /* seg003: безоружный Кид отскакивает от стража */
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */ check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action(); check_action();
+33
View File
@@ -12,6 +12,7 @@
#define POP_MAP_H #define POP_MAP_H
#include <stdint.h> #include <stdint.h>
#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10). /* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив * 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. */ * 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; 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_curr;
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */ extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_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; void pop_kid_hp_reset(void) __banked;
/* То же для БЕСШОВНОГО старта уровня (pop_seamless): HP не трогаем вовсе. */
void pop_kid_hp_carry(void) __banked;
/* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт /* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт
* уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень * уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень
@@ -89,8 +102,14 @@ extern uint8_t pop_flash_time;
#define POP_FLASH_YELLOW 0 #define POP_FLASH_YELLOW 0
#define POP_FLASH_RED 1 #define POP_FLASH_RED 1
#define POP_FLASH_GREEN 2 #define POP_FLASH_GREEN 2
#define POP_FLASH_WHITE 3 /* тень: слияние и её смерть (уровень 12) */
extern uint8_t pop_flash_color; extern uint8_t pop_flash_color;
/* add_life (seg000:11EC): +1 к ПОТОЛКУ HP и долив до него. Наружу — ради
* слияния с тенью (уровень 12, guards.c в другом банке): большая склянка
* зовёт ту же ветку изнутри модуля. */
void pop_add_life(void) __banked;
/* is_feather_fall: кадров, пока действует зелье медленного падения. Читают /* is_feather_fall: кадров, пока действует зелье медленного падения. Читают
* play_seq (опкод JMP_IF_FEATHER ветки stepfloat/bumpfloat в seqtbl) и * play_seq (опкод JMP_IF_FEATHER ветки stepfloat/bumpfloat в seqtbl) и
* приложение (сброс на старте уровня, seg003:189). */ * приложение (сброс на старте уровня, seg003:189). */
@@ -127,6 +146,20 @@ extern uint8_t pop_leave_timer;
extern uint8_t pop_loose_modif[30]; extern uint8_t pop_loose_modif[30];
void pop_loose_tick(void) __banked; void pop_loose_tick(void) __banked;
void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */ 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-пол упал (кусок улетел ВНИЗ). /* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */ * Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
+216 -14
View File
@@ -28,6 +28,18 @@
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
/* Фазы 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): оригинал под один * Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один
* и тот же набор id грузит РАЗНЫЙ .DAT VDUNGEON или VPALACE. У нас так * и тот же набор id грузит РАЗНЫЙ .DAT VDUNGEON или VPALACE. У нас так
@@ -809,9 +821,17 @@ void pop_loose_bake_empty(int row, int col) __banked
* куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе * куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе
* подряд, и второй спавн затирал состояние первого его пиксели оставались * подряд, и второй спавн затирал состояние первого его пиксели оставались
* висеть в воздухе (на каждой странице на своей y мерцание), а щебень * висеть в воздухе (на каждой странице на своей 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 { typedef struct {
uint8_t active; /* 0 нет; 1 падает */ uint8_t active; /* 0 нет; 1 падает */
uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */ uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */
@@ -823,7 +843,12 @@ typedef struct {
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */ uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
} mob_t; } mob_t;
static mob_t mobs[MOB_MAX]; /* НЕ static намеренно: статики банкового модуля в карту символов не
* попадают, и разобрать зависший кадр по слотам (y каждого куска, порядок,
* defer) в отладчике нечем адреса просто нет. Снято 2026-08-13 для
* разбора артефактов множественного падения; наружу таблица не объявлена и
* никем не читается. */
mob_t mobs[MOB_MAX];
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на /* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
* который он лёг приложение/pop_map ставит debris + do_knock. */ * который он лёг приложение/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}; static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF #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; uint8_t i;
mob_t *m = 0; 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->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
m->row = (int8_t)row; m->row = (int8_t)row;
m->col = (int8_t)col; 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->speed = 0;
m->active = 1; m->active = 1;
m->clean = 0; 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; 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) /* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
* с mob_x+32 ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46: * с mob_x+32 ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину * ширина 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); 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) static void mob_tick_one(mob_t *m, uint8_t pg)
{ {
uint8_t here; uint8_t here;
@@ -921,6 +1018,19 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* на одной из двух страниц дабл-буфера «застрявшая плита», видимая * на одной из двух страниц дабл-буфера «застрявшая плита», видимая
* через кадр (BUG-MOB-STALE-PAGE). */ * через кадр (BUG-MOB-STALE-PAGE). */
if (m->prev_y[pg] != MOB_Y_NONE) { 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); pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
m->prev_y[pg] = MOB_Y_NONE; 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; if (m->speed < 29) m->speed += 3;
m->y += m->speed; m->y += m->speed;
/* move_loose (seg007:1126): низ куска пересёк границу своего ряда — /* move_loose (seg007:1126): низ куска пересёк границу своего ряда —
* смотрим тайл этого ряда. Пусто/loose валится дальше (порт * смотрим тайл этого ряда. Пусто/loose валится дальше, пол
* loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); * приземление (loose_land: debris + do_knock). Через LOOSE кусок именно
* пол приземление (loose_land: debris + do_knock). */ * ПРОЛЕТАЕТ, как через пустоту, но по дороге СРЫВАЕТ её (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) { if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
uint8_t t = mob_tile_at(m); uint8_t t = mob_tile_at(m);
if (t == 11) mob_break_loose(m);
if (t == 0 || t == 11) { if (t == 0 || t == 11) {
/* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда — /* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда —
* в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает * в комнату СНИЗУ (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); 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) { } 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 */ 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 if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col);
else over = (m->y > pop_cd[POP_CH_KID].fpy); 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 void pop_loose_mob_tick(void) __banked
@@ -1010,6 +1174,15 @@ void pop_loose_mob_tick(void) __banked
* (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а * (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а
* стереть себя ему всё равно надо будет. */ * стереть себя ему всё равно надо будет. */
if (mobs[i].active || mobs[i].clean) 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); pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64);
mob_tick_one(&mobs[i], pg); mob_tick_one(&mobs[i], pg);
} }
@@ -1042,6 +1215,24 @@ static void mob_render(mob_t *m, uint8_t pg)
* границе рядов. Порт draw_mob (seg007:13E5). */ * границе рядов. Порт draw_mob (seg007:13E5). */
int8_t r = pop_y_to_row((int16_t)m->y); int8_t r = pop_y_to_row((int16_t)m->y);
int8_t rt = pop_y_to_row((int16_t)(m->y - 18)); 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; /* полоса у потолка — только срез */ if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */
draw_tile(r, m->col + 1); draw_tile(r, m->col + 1);
pop_t_clip_top = 0; 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); draw_tile(rt, m->col + 1);
pop_t_clip_top = 0; pop_t_clip_top = 0;
} }
pop_t_fclip_on = 0;
} }
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */ /* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */
pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30); 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). Звать /* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
* после отрисовки персонажей, до fore-слоя. */ * после отрисовки персонажей, до fore-слоя. */
void pop_loose_mob_draw_over(void) __banked void pop_loose_mob_draw_over(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1, i; mob_draw_pass(gfx_get_draw_page() & 1, 1);
for (i = 0; i < MOB_MAX; i++)
if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); }
} }
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и /* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
+21
View File
@@ -81,3 +81,24 @@ void pop_dbg_trap(void) { pop_dbg_hits++; }
/* Зеркало уровня 4 — см. pop_state.h. */ /* Зеркало уровня 4 — см. pop_state.h. */
int8_t pop_jumped_mirror; 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;
+13
View File
@@ -20,6 +20,19 @@ extern uint8_t pop_loose_landed;
extern uint8_t pop_loose_exit; extern uint8_t pop_loose_exit;
extern uint8_t pop_loose_exit_room; 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 не убавляется. Читают пути урона /* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
* (боёвка в банке, пики в pop_map), переключает главный цикл. */ * (боёвка в банке, пики в pop_map), переключает главный цикл. */
extern uint8_t pop_immortal; extern uint8_t pop_immortal;
+53
View File
@@ -14,6 +14,7 @@
* пометки, поэтому pop_process_trobs единственная функция модуля, которую * пометки, поэтому pop_process_trobs единственная функция модуля, которую
* нельзя звать из __banked-кода. */ * нельзя звать из __banked-кода. */
#include "pop_bg.h" #include "pop_bg.h"
#include "pop_tile.h" /* pop_palace — тип уровня как модификатор дыры */
#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */ #include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */
#include "pop_kid.h" /* Kid.curr_row/alive — условие остановки чомпера */ #include "pop_kid.h" /* Kid.curr_row/alive — условие остановки чомпера */
#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */ #include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */
@@ -31,6 +32,7 @@
#define TILE_CHOMP 0x12 /* чомпер (челюсти) */ #define TILE_CHOMP 0x12 /* чомпер (челюсти) */
#define TILE_TORCH 0x13 #define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E #define TILE_TORCH_DEBRIS 0x1E
#define TILE_EMPTY 0x00 /* remove_loose: на месте провалившейся плиты */
/* ---- Персистентное per-room modifier-состояние -------------------- */ /* ---- Персистентное per-room modifier-состояние -------------------- */
static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES]; 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; *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) --------------------------------------- * /* ---- Кнопки/ворота (seg007) --------------------------------------- *
* Таблицы связей LINKLOC/LINKMAP в pop_level (мутабельная копия в W2). * Таблицы связей LINKLOC/LINKMAP в pop_level (мутабельная копия в W2).
* doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */ * doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */
@@ -574,6 +624,9 @@ void pop_process_trobs(uint8_t cur_room) __banked
case TILE_GATE: case TILE_GATE:
animate_door(&mod[tp], &type); animate_door(&mod[tp], &type);
break; break;
case TILE_LOOSE:
animate_loose(&mod[tp], &type, room, tp, cur_room);
break;
case TILE_LEVELDOOR: case TILE_LEVELDOOR:
animate_leveldoor(&mod[tp], &type, cur_room); animate_leveldoor(&mod[tp], &type, cur_room);
break; break;
+25
View File
@@ -126,4 +126,29 @@
* чтение 34 страниц пришлась на загрузку уровня, а не на глоток зелья. */ * чтение 34 страниц пришлась на загрузку уровня, а не на глоток зелья. */
#define POP_UPSIDE_LEVEL 9 #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 */ #endif /* POP_TUNE_H */
+56
View File
@@ -355,6 +355,8 @@ int main(void)
} }
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */ pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы — /* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
@@ -396,6 +398,7 @@ int main(void)
pop_next_level = (uint8_t)(pop_current_level + 1); pop_next_level = (uint8_t)(pop_current_level + 1);
} }
else if (down) { /* L2: переход в комнату снизу — продолжает падать */ else if (down) { /* L2: переход в комнату снизу — продолжает падать */
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
enter_room(down); /* координаты Kid уже репроецированы pop_map */ enter_room(down); /* координаты Kid уже репроецированы pop_map */
gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */ gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */
} else { /* выпал из уровня (нет комнаты снизу) — рестарт */ } else { /* выпал из уровня (нет комнаты снизу) — рестарт */
@@ -424,9 +427,20 @@ int main(void)
pop_checkpoint = 1; pop_checkpoint = 1;
hitp_beg_lev = hitp_max; 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) { if (nb) {
/* side для pop_guard_follow — то же направление, по которому /* side для pop_guard_follow — то же направление, по которому
* ищем соседа (roomleave_result оригинала). */ * ищем соседа (roomleave_result оригинала). */
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1)); enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1));
gfx_set_draw_page(back); gfx_set_draw_page(back);
} }
@@ -438,6 +452,30 @@ int main(void)
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */ /* — ДО Kid, чтобы Kid всегда был поверх плиты) */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */ pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */ /* 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(); /* и разобрать пометки перерисовки тайлов (порт */ pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */ /* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
{ /* ворота соседа слева (room8) видны в col0 — */ { /* ворота соседа слева (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) { if (pop_next_level != pop_current_level) {
/* Смена уровня целиком — в банке (pop_level_switch): грузится /* Смена уровня целиком — в банке (pop_level_switch): грузится
* файл, спрайты соперника и тайлсет. Раз на уровень. */ * файл, спрайты соперника и тайлсет. Раз на уровень. */
@@ -550,6 +600,7 @@ int main(void)
* HP красный, перо (медленное падение) зелёный. */ * HP красный, перо (медленное падение) зелёный. */
if (pop_flash_color == POP_FLASH_RED) flash_bg(255, 0, 0); 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_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); else flash_bg(255, 255, 85);
pop_flash_time--; pop_flash_time--;
if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW; if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW;
@@ -567,6 +618,11 @@ int main(void)
* пролетел сквозь зеркало, тень должна родиться ДО того, как слот * пролетел сквозь зеркало, тень должна родиться ДО того, как слот
* соперника пойдёт рисоваться, иначе кадр её проглотит. */ * соперника пойдёт рисоваться, иначе кадр её проглотит. */
pop_check_mirror(); pop_check_mirror();
/* Падающие плиты, идущие ПОД Kid. Здесь, а не внутри pop_loose_tick:
* фон кадра (перепечатки тайлов, шов) обязан быть готов ДО любого
* спрайта, иначе перепечатка ложится поверх уже нарисованного куска
* так окно задней стены и передняя грань пола оказывались на плите. */
pop_loose_mob_draw();
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char * персонажа (см. guard_over_kid). Отрисовка одна на всех Char
* (pop_cdraw.c), различает их только слот. */ * (pop_cdraw.c), различает их только слот. */
+28 -1
View File
@@ -109,7 +109,15 @@ void pop_start_level(void) __banked
pop_level_reset_tiles(); pop_level_reset_tiles();
pop_level_reset_guards(); pop_level_reset_guards();
pop_guard_reset(); /* start_level: Guard.direction = none */ 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_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */
pop_loose_forget(); /* ...и фазы плит — уровень восстановлен целиком */
/* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в /* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в
* комнату-чекпойнт после респавна остался бы непройденным. */ * комнату-чекпойнт после респавна остался бы непройденным. */
if (chkp) if (chkp)
@@ -155,7 +163,11 @@ void pop_start_level(void) __banked
/* play_level (seg003:106): have_sword = level >= have_sword_from_level /* play_level (seg003:106): have_sword = level >= have_sword_from_level
* (=2) начиная со второго уровня меч у Кида уже есть. */ * (=2) начиная со второго уровня меч у Кида уже есть. */
pop_have_sword = (uint8_t)(pop_current_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; holding_sword = 0;
offguard = 0; offguard = 0;
guard_refrac = 0; guard_refrac = 0;
/* ПРОВАЛ НА СЛЕДУЮЩИЙ УРОВЕНЬ ПО ЧИТУ (та же болезнь, что выше).
* Уровень 12 кончается ФАКТОМ попадания в комнату 23 двери у него нет
* (pop_seamless, seg000:0900). А навигация перебирает комнаты ПО
* НОМЕРУ, то есть любой обход рано или поздно наступает на 23-ю, и
* уровень молча меняется на 13-й: комнату 23 двенадцатого уровня читом
* не посмотреть в принципе, а прогон «съезжает» на соседний уровень
* незаметно для проверяющего (поймано 2026-08-13 на первом же прогоне).
* Держим триггер, пока Кид не сменит комнату НОГАМИ. */
pop_nav_hold = 1;
return 1; return 1;
} }
@@ -276,6 +298,10 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
* enter звать нельзя: он уже перенесён и живёт дальше как есть. */ * enter звать нельзя: он уже перенесён и живёт дальше как есть. */
uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0); uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0);
if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */ if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */
/* ...и так же запомнить НЕДОСЧИТАННЫЕ loose: пока карта ещё описывает
* СТАРУЮ комнату, фаза уезжает в персистентный room_modif + trob, и
* плита доваливается заочно (см. pop_loose_leave_room / animate_loose). */
pop_loose_leave_room();
uint8_t *room_mod; uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
pop_map_set(room_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_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */ pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */
pop_check_fall_flo(); /* спецсобытие ур.13: гряда плит сверху */
/* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»- /* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»-
* уровнях забирает слот соперника себе: вернула 1 стража поднимать * уровнях забирает слот соперника себе: вернула 1 стража поднимать
* НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда * НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда
@@ -49,6 +49,12 @@ OBJS_char := $(OBJS_phys)
# (pop_check_mouse) и общей физикой соперника — то есть ровно то, что в # (pop_check_mouse) и общей физикой соперника — то есть ровно то, что в
# остальных наборах заменено ручным прогоном play_guard_frame. # остальных наборах заменено ручным прогоном play_guard_frame.
OBJS_mouse := $(OBJS_phys) build/eng_guards.rel 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 include $(TESTKIT)/host-tests.mk
@@ -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) 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(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(); pop_kid_hp_reset();
} }
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir) 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_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
Kid.room = tk_start_room;
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */ Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */
/* Колонку пересчитываем В ОКНЕ Char: determine_col — часть физики, а та /* Колонку пересчитываем В ОКНЕ Char: determine_col — часть физики, а та
* работает с активным персонажем (порт load_fram_det_col). */ * работает с активным персонажем (порт load_fram_det_col). */
+3 -3
View File
@@ -223,12 +223,12 @@ uint8_t pop_level_guard_hp(uint8_t level) __banked { (void)level; return 3; }
/* Тайлы «уровня» для соседних комнат: pop_map_set_edges забирает fg соседей /* Тайлы «уровня» для соседних комнат: 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_begin(void) { }
void pop_level_access_end(void) { } void pop_level_access_end(void) { }
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos) 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]; 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). */ * пор, как do_pickup правит страницу уровня сразу (BUG-POTION-STRIPE). */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile) void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
{ {
if (room == 0 || room >= TK_ROOMS || tilepos >= 30) return; if (room == 0 || room > TK_ROOMS || tilepos >= 30) return;
tk_room_fg[room][tilepos] = tile; tk_room_fg[room][tilepos] = tile;
} }
+6 -2
View File
@@ -42,7 +42,11 @@ uint8_t tk_log_count(uint8_t kind);
/* Состояние стражей по комнатам для follow_guard (см. stubs.c). */ /* Состояние стражей по комнатам для 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_guard_room; /* комната, где страж есть «по данным уровня» */
extern uint8_t tk_gs_tile[TK_ROOMS]; extern uint8_t tk_gs_tile[TK_ROOMS];
extern uint16_t tk_gs_seq[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; extern uint8_t tk_link_down;
/* fg комнат «уровня» — источник для кэша соседей в pop_map_set_edges. */ /* 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 #endif
@@ -29,6 +29,7 @@
#define E 0 /* empty */ #define E 0 /* empty */
#define F 1 /* floor */ #define F 1 /* floor */
#define G 4 /* gate (portcullis) */
#define W 20 /* wall */ #define W 20 /* wall */
/* Ровный пол на ряду 1 (как room_flat в t_phys). */ /* Ровный пол на ряду 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, 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: кому достаётся урон ----------------------------------- */ /* ---- take_hp: кому достаётся урон ----------------------------------- */
TC_TEST(char_take_hp_goes_to_active) 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__); 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) void main(void)
{ {
sc_init(); sc_init();
@@ -457,4 +516,8 @@ void main(void)
TC_RUN(char_tiles_resolve_across_rooms); TC_RUN(char_tiles_resolve_across_rooms);
TC_RUN(char_skeleton_reappears_below); TC_RUN(char_skeleton_reappears_below);
TC_RUN(char_skeleton_falls_out_elsewhere); 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);
} }
@@ -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 <stdint.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_state.h"
#include "pop_level.h"
#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);
}
+34 -2
View File
@@ -211,13 +211,44 @@ TC_TEST(phys_loose_floor_breaks)
/* Журнал заглушек: плита обязана была отвалиться (кусок отрывается — /* Журнал заглушек: плита обязана была отвалиться (кусок отрывается —
* pop_loose_mob_spawn) и попросить перерисовку. Это уже не * 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_MOB_SPAWN) > 0);
TC_TRUE(tk_log_count(TK_REDRAW) > 0); TC_TRUE(tk_log_count(TK_REDRAW) > 0);
#endif #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) TC_TEST(phys_running_jump_over_3tile_gap)
{ {
/* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок. /* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок.
@@ -295,6 +326,7 @@ void main(void)
TC_RUN(phys_jump_up_nothing_above); TC_RUN(phys_jump_up_nothing_above);
TC_RUN(phys_run_off_ledge); TC_RUN(phys_run_off_ledge);
TC_RUN(phys_loose_floor_breaks); TC_RUN(phys_loose_floor_breaks);
TC_RUN(phys_loose_survives_room_change);
TC_RUN(phys_running_jump_over_3tile_gap); TC_RUN(phys_running_jump_over_3tile_gap);
TC_RUN(phys_feather_fall_is_slow_and_harmless); TC_RUN(phys_feather_fall_is_slow_and_harmless);
} }
@@ -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 <stdint.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_state.h"
#include "pop_level.h"
#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);
}
+15 -9
View File
@@ -29,6 +29,7 @@ from pop_pack_kid import transpose_cols, vflip_page
SETS = { SETS = {
"GUARD": ("GUARD", "guard", None), "GUARD": ("GUARD", "guard", None),
"SKEL": ("SKEL", "skel", "res750.pal"), "SKEL": ("SKEL", "skel", "res750.pal"),
"VIZIER": ("VIZIER", "vizier", "res750.pal"),
} }
SET_NAME = "GUARD" SET_NAME = "GUARD"
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "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): def select_set(name):
"""Переключить упаковщик на набор (GUARD/SKEL): каталоги и число кадров.""" """Переключить упаковщик на набор (GUARD/SKEL/VIZIER): каталоги и число
кадров."""
global SET_NAME, GUARD_DIR, OUT_DIR, MAX_ID global SET_NAME, GUARD_DIR, OUT_DIR, MAX_ID
if name not in SETS: if name not in SETS:
B.die("неизвестный набор %s (есть: %s)" % (name, ", ".join(SETS))) B.die("неизвестный набор %s (есть: %s)" % (name, ", ".join(SETS)))
@@ -142,19 +144,23 @@ def write_pal_header(out_path):
"};", "};",
"", "",
] ]
# Палитра скелета — из ЕГО набора (res750.pal), а не из res10.bin: на # Палитры наборов со СВОИМ цветом (скелет, визирь) — из их res750.pal, а
# уровне 3 curr_guard_color = 0, оригинал палитру не подменяет вовсе. # не из res10.bin: на таких уровнях curr_guard_color = 0 (seg002:186), и
# оригинал палитру не подменяет вовсе.
saved = SET_NAME saved = SET_NAME
select_set("SKEL") for name, cname, human in (("SKEL", "pop_skel_pal", "Скелет"),
skel = set_palette() ("VIZIER", "pop_vizier_pal", "Джафар")):
select_set(name)
pal = set_palette()
select_set(saved) select_set(saved)
if skel: if not pal:
continue
vals = [] vals = []
for (r, g, b) in skel: for (r, g, b) in pal:
vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0] vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0]
lines += [ lines += [
"/* Скелет (SKEL/res750.pal): свой набор — свой цвет. */", "/* %s (%s/res750.pal): свой набор — свой цвет. */" % (human, name),
"static const uint8_t pop_skel_pal[64] = {", "static const uint8_t %s[64] = {" % cname,
" " + ",".join("0x%02X" % v for v in vals) + ",", " " + ",".join("0x%02X" % v for v in vals) + ",",
"};", "};",
"", "",