Уровни 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:
@@ -11,6 +11,7 @@
|
||||
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему |
|
||||
| [`perf_backlog.md`](perf_backlog.md) | **Отложенная оптимизация отрисовки** с замерами + как мерить (wait-state'ы, границы кадра) |
|
||||
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
||||
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
|
||||
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
|
||||
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди |
|
||||
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии из `BUGS_CLOSED.md`, дифф против SDLPoP |
|
||||
|
||||
@@ -245,3 +245,162 @@ offscreen неперевёрнутым, а зеркалит только при
|
||||
**Что проверять при регрессе.** Уровень 9: выпить зелёное зелье — картинка
|
||||
переворачивается ровно один раз, лишних языков пламени по комнате нет. Чит
|
||||
U даёт тот же результат (он идёт тем же путём).
|
||||
|
||||
---
|
||||
|
||||
## Окклюзия воротами: спрашиваем про рисуемого персонажа, а не жёстко про Кида
|
||||
|
||||
**Оригинал** (`draw_tile_fore`, seg008:0D15) первой строкой:
|
||||
|
||||
```c
|
||||
if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
|
||||
Kid.curr_col == drawn_col - 1 && Kid.room != room_R)
|
||||
draw_gate_fore();
|
||||
```
|
||||
|
||||
То есть бары ворот попадают в foretable — поверх всего нарисованного — когда
|
||||
на тайле ворот стоит **именно Кид**. Это следствие устройства оригинала:
|
||||
foretable ОДНА на весь проход тайлов, персонажи в неё уже добавлены, и
|
||||
отдельного «переднего слоя на персонажа» там нет.
|
||||
|
||||
**Мы** ради скорости не рисуем foretable целиком, а возвращаем куски тайлов
|
||||
поверх ТОЛЬКО в прямоугольнике персонажа (`pop_fore_over_char`, см. memory
|
||||
`pop_fore_layer_cost`: полный проход стоил 78 % кадра). Проход идёт по
|
||||
персонажу, значит и вопрос естественно задавать про него —
|
||||
`pop_gate_over_char(ch)`, а не про глобального `Kid`.
|
||||
|
||||
**Чем платим.** Наш вариант — надмножество оригинального: страж (или тень),
|
||||
стоящий в проёме ворот, у нас уходит ЗА решётку, а в оригинале остался бы
|
||||
нарисованным поверх неё, пока на том же тайле нет Кида. Визуально это
|
||||
правильнее, но формально расхождение. Обратной разницы нет: во всех случаях,
|
||||
где оригинал рисует бары поверх, рисуем и мы.
|
||||
|
||||
**Что проверять при регрессе.** Уровень 10, комната 7, тайл (2,6): Кид,
|
||||
стоящий в проёме ворот, виден ЗА прутьями. Решение покрыто хост-тестами
|
||||
(`tests-host/t_char.c`, набор `char_gate_*`) — отрисовка в харнесс не
|
||||
линкуется, поэтому проверяется предикат.
|
||||
|
||||
---
|
||||
|
||||
## Слияние с тенью: мигания Кида спрайтами тени нет
|
||||
|
||||
**Файлы:** `roomtest/guards.c` (`autocontrol_shadow_level12`),
|
||||
`roomtest/pop_cdraw.c`.
|
||||
**Дата:** 2026-08-13.
|
||||
|
||||
**Оригинал** (`draw_objtable_item`, seg008:20CA) во время вспышки слияния
|
||||
(`united_with_shadow` считает 42 → 0) рисует КИДА как тень на чётных
|
||||
значениях счётчика: тот же кадр уходит не обычным прозрачным блиттером, а
|
||||
парой OR+XOR со сдвигом на пиксель. Получается мерцание «Кид/тень»
|
||||
примерно полторы секунды.
|
||||
|
||||
**Мы** рисуем всё это время обычного Кида, а само событие обозначаем белой
|
||||
вспышкой фона (`pop_flash_color = POP_FLASH_WHITE`, 18 кадров) — она в
|
||||
оригинале тоже есть и ставится тем же кодом.
|
||||
|
||||
**Почему.** У нас Кид и соперник рисуются из РАЗНЫХ атласов своими
|
||||
палитрами (`pop_cdraw.c`), а «тень» — это персонаж слота Guard с палитрой
|
||||
комнаты; блиттеров OR/XOR в libbgi нет вовсе, прозрачность сделана
|
||||
0xFF-подавлением записи. Воспроизвести эффект — значит завести Киду второй
|
||||
набор спрайтов и второй путь блита ради 42 кадров за всю игру.
|
||||
|
||||
**Чем платим.** Момент слияния читается только по вспышке и по тому, что
|
||||
тень исчезла, — без «двоящегося» силуэта. На механику не влияет: счётчик
|
||||
`pop_united_shadow` тикает и уходит в −1 независимо от отрисовки, а от него
|
||||
зависят и повторный подъём тени, и появление плит в комнатах 2/13.
|
||||
|
||||
**Что проверять при регрессе.** Уровень 12: после слияния экран белеет,
|
||||
соперник пропал, HP-потолок вырос на единицу, тень в комнате 15 больше не
|
||||
появляется. Логика покрыта `tests-host/t_shadow.c`.
|
||||
|
||||
---
|
||||
|
||||
## Отложенный старт падающих плит (уровень 13): фаза 0 у нас «не анимируется»
|
||||
|
||||
**Файлы:** `roomtest/pop_map.c` (`pop_check_fall_flo`, `pop_loose_tick`),
|
||||
`roomtest/pop_trob.c` (`animate_loose`).
|
||||
**Дата:** 2026-08-13.
|
||||
|
||||
**Оригинал** (`check_fall_flo`, seg000:1317) раздаёт шести плитам ряда 2
|
||||
верхней комнаты модификатор `−(prandom(0xFF) & 0x0F)`, то есть 0..−15, и
|
||||
заводит на каждую trob. Фаза считает вверх, проходит ноль и дальше идёт
|
||||
обычным отсчётом до провала — плита падает через `n + 11` кадров. Ноль там
|
||||
безопасен: плиту держит в игре СПИСОК trob, а не значение модификатора.
|
||||
|
||||
**Мы** списка trob для loose текущей комнаты не держим — плита анимируется
|
||||
ровно тогда, когда её фаза не ноль (`pop_loose_modif[pos] != 0`). Значит
|
||||
счёт, дойдя до нуля, оборвался бы навсегда. Компенсируем двумя правками,
|
||||
которые работают только в паре:
|
||||
|
||||
* тик перескакивает ноль (`if (m == 0) m = 1`);
|
||||
* стартовое значение берётся на единицу «отрицательнее» (`−n−1`).
|
||||
|
||||
**Чем платим.** Ничем в наблюдаемом поведении: суммарная задержка остаётся
|
||||
`n + 11` кадров, проверено арифметикой на обоих концах диапазона (n = 0 и
|
||||
n = 15). Платим связностью — две правки в разных функциях, и убрать любую
|
||||
одну нельзя.
|
||||
|
||||
**Что проверять при регрессе.** Уровень 13, вход в комнату 23 (она же
|
||||
стартовая): плиты сверху сыплются ВРАЗНОБОЙ, а не разом и не «никогда».
|
||||
Логика покрыта `tests-host/t_jaffar.c`
|
||||
(`jaffar_negative_phase_counts_through_to_fall` и парный контроль на другом
|
||||
уровне).
|
||||
|
||||
---
|
||||
|
||||
## Чит навигации по комнатам не запускает бесшовный переход уровня
|
||||
|
||||
**Файлы:** `roomtest/roomtest_cold.c` (`pop_dbg_roomnav`), `roomtest/roomtest.c`,
|
||||
`roomtest/pop_state.c` (`pop_nav_hold`).
|
||||
**Дата:** 2026-08-13.
|
||||
|
||||
**Оригинал** (`play_level_2`, seg000:0900) проверяет `Kid.room == 23` КАЖДЫЙ
|
||||
кадр: уровень 12 кончается самим фактом присутствия Кида в комнате 23, двери
|
||||
у него нет. Никакого «как он туда попал» там нет и быть не может —
|
||||
телепорта между комнатами в игре 1989 года не существует.
|
||||
|
||||
**Мы** держим этот триггер, пока Кид попал в комнату ЧИТОМ навигации
|
||||
(`+`/`−`), и отпускаем на первой же смене комнаты обычным ходом.
|
||||
|
||||
**Почему.** Чит перебирает комнаты ПО НОМЕРУ (1..24 с обёрткой), то есть
|
||||
любой обход уровня 12 неизбежно наступает на 23-ю — и уровень молча
|
||||
становится 13-м. Поймано на первом же прогоне 2026-08-13: проверяющий час
|
||||
смотрел «комнату 20 уровня 12», которая на самом деле была комнатой 20
|
||||
уровня 13, и сравнивал её с картой не того уровня. Комнату 23 уровня 12
|
||||
читом не посмотреть в принципе. Это ровно та же болезнь чит-телепорта, что
|
||||
BUG-CHEAT-FIGHT-1 (выход из боя), и лечится там же.
|
||||
|
||||
**Чем платим.** Ничем в игре: в обычном прохождении Кид входит в комнату 23
|
||||
ногами, флаг снят, переход срабатывает как в оригинале. Расхождение видно
|
||||
ТОЛЬКО при включённых читах.
|
||||
|
||||
**Что проверять при регрессе.** Уровень 12: пройти в комнату 23 ногами —
|
||||
уровень меняется на 13-й без заставки и без сброса HP. Обойти уровень
|
||||
читом `+` через 23-ю — уровень НЕ меняется.
|
||||
|
||||
---
|
||||
|
||||
## Чит «убить стража» (K) идёт через штатный путь смерти
|
||||
|
||||
**Файлы:** `roomtest/pop_guard.c` (`pop_guard_kill`).
|
||||
**Дата:** 2026-08-13. Решение пользователя.
|
||||
|
||||
**Оригинал** (seg000:786) ставит `guardhp_delta = -guardhp_curr` И
|
||||
`Guard.alive = 0`. А гейт события смерти в `play_guard` (seg006:1490)
|
||||
требует `Char.alive < 0` — то есть у оригинала чит убивает стража В ОБХОД
|
||||
`on_guard_killed`. На 13-м уровне это заметно: победа над Джафаром по читу
|
||||
не ставит `leveldoor_open = 2`, и выход на 14-й не открывается.
|
||||
|
||||
**Мы** `Guard.alive` в чите не трогаем: применённая дельта обнуляет HP, и
|
||||
`play_guard` сам переводит стража в «умирает», вызвав `on_guard_killed` —
|
||||
брызги, вспышка, флаг выхода. То есть чит даёт ровно «как будто убил Кид».
|
||||
|
||||
**Почему.** Отладочный прогон 13-го уровня иначе требует каждый раз честно
|
||||
выигрывать бой с Джафаром (skill 9, 6 HP) — это дорого по времени, а
|
||||
проверять надо совсем другое.
|
||||
|
||||
**Чем платим.** Ничем в игре: читы включаются флагом `pop_cheats`, в
|
||||
релизной сборке они выключены. Расхождение наблюдаемо только с читами.
|
||||
|
||||
**Что проверять при регрессе.** Уровень 13: `K` на Джафаре → белая вспышка,
|
||||
уход ВЛЕВО открывает дверь уровня. Честная победа в бою даёт то же самое.
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
# Что осталось на уровнях 12, 13, 14, 15 и 0
|
||||
|
||||
Статус: разбор по `../SDLPoP/src/`, 2026-08-13. Продолжает
|
||||
[`levels_plan.md`](levels_plan.md) (машинерия уровней и тайлсеты — уже
|
||||
сделаны). Здесь только СПЕЦСОБЫТИЯ, которых у нас ещё нет.
|
||||
|
||||
Правило проекта: источник истины — SDLPoP; все ссылки ниже даны на функцию и
|
||||
строку, чтобы порт начинался с чтения, а не с гипотезы.
|
||||
|
||||
---
|
||||
|
||||
## 0. Что из этой области УЖЕ есть
|
||||
|
||||
Проверено грепом по `roomtest/`:
|
||||
|
||||
| механика | где у нас | статус |
|
||||
|---|---|---|
|
||||
| слот соперника, общий на всех Char | `pop_guard.c`, `pop_cdraw.c` | готово |
|
||||
| `check_shadow` (спецвход тени) | `guards.c:310` | готово для уровней 4/5/6 |
|
||||
| `do_init_shad` + таблицы `init_shad_5/6` | `guards.c:284` | готово |
|
||||
| ИИ тени 4/5/6 | `guards.c:599/658/680` | готово |
|
||||
| диспетчер `autocontrol_shadow` | `guards.c:711` | ветки 12 НЕТ |
|
||||
| боёвка (удар/блок/парирование/HP соперника) | `guards.c`, `pop_ctrl.c` | готово |
|
||||
| `flash_color` / `flash_time` | `pop_map.c`, `roomtest.c` | готово |
|
||||
| `add_life` | `pop_map.c` | готово |
|
||||
| таблицы уровней (`guard_type`, `guard_hp`, `entry_pose`, `level_type`) | `pop_level_cold.c:41..52` | готово, включая 12=SHADOW, 13=VIZIER |
|
||||
| loose-полы, `make_loose_fall`, mob | `pop_map.c`, `pop_room.c` | готово (без спецкейсов ур. 13) |
|
||||
|
||||
То есть каркас есть весь; ниже — недостающие спецсобытия.
|
||||
|
||||
---
|
||||
|
||||
## 1. Уровень 12 — тень: встреча, бой, слияние
|
||||
|
||||
### 1.1 Подъём тени в комнате 15 (`check_shadow`, seg002:0070)
|
||||
|
||||
```c
|
||||
if (current_level == 12) {
|
||||
if (!united_with_shadow && drawn_room == 15) {
|
||||
Char.room = drawn_room;
|
||||
if (get_tile(15, 1, 0) == tiles_22_sword) return; // меч ещё лежит
|
||||
shadow_initialized = 0;
|
||||
do_init_shad(init_shad_12, 7 /* fall */);
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Отличия от наших веток 5/6: тень поднимается **в падении** (seq 7), условием
|
||||
служит содержимое тайла (меч уже подобран) и флаг `united_with_shadow`.
|
||||
Таблица `init_shad_12 = {0x0F, 0x51, 0xE8, 0, 0, 0, 0, 0}` — то есть
|
||||
x=81, y=232, вправо, колонка 0, ряд 0.
|
||||
|
||||
**Что добавить:** ветку в `pop_check_shadow` + константу `init_shad_12` +
|
||||
глобалы `united_with_shadow`, `shadow_initialized`.
|
||||
|
||||
### 1.2 ИИ тени (`autocontrol_shadow_level12`, seg002:1184)
|
||||
|
||||
Самая содержательная функция уровня. Три режима:
|
||||
|
||||
1. **Первый кадр в комнате 15**: пока Кид не подошёл (`Opp.x < 150`) —
|
||||
`shadow_initialized = 1`; иначе тень ещё раз падает (`do_init_shad`).
|
||||
2. **Кид с мечом** (`Char.sword >= sword_2_drawn`) → тень дерётся обычным
|
||||
`autocontrol_guard_active` (у нас есть, `guards.c:535`). Особый случай:
|
||||
если тень уже ранена (`offguard != 0 && guard_refrac != 0`) — она убирает
|
||||
меч (`move_4_down`).
|
||||
3. **Кид убрал меч** → тень тоже убирает и идёт навстречу; на дистанции
|
||||
`< 10` — **СЛИЯНИЕ**:
|
||||
|
||||
```c
|
||||
flash_color = color_15_brightwhite; flash_time = 18;
|
||||
add_life(); // +1 к максимуму HP
|
||||
united_with_shadow = 42; // время вспышки Кид-тень
|
||||
Char.charid = charid_0_kid; savekid(); // Кид ПЕРЕЕЗЖАЕТ на место тени
|
||||
clear_char(); // тень со сцены
|
||||
```
|
||||
|
||||
Плюс «если Кид бежит к тени — тень бежит к Киду» (кадры бега 3..14 и
|
||||
шага 127..132).
|
||||
|
||||
**Что добавить:** `autocontrol_shadow_level12` в `guards.c` + ветку в
|
||||
диспетчере `autocontrol_shadow` (там уже три ветки, будет четвёртая).
|
||||
|
||||
### 1.3 Общий урон (`do_delta_hp`, seg000:1518)
|
||||
|
||||
```c
|
||||
if (Opp.charid == charid_1_shadow && current_level == 12 && guardhp_delta != 0)
|
||||
hitp_delta = guardhp_delta; // ранил тень — ранил себя
|
||||
```
|
||||
|
||||
Три строки, но без них бой с тенью теряет смысл. У нас `do_delta_hp`
|
||||
портирован — добавить условие.
|
||||
|
||||
### 1.4 Таймер вспышки (`do_timers`, seg003:503)
|
||||
|
||||
```c
|
||||
if (united_with_shadow > 0) {
|
||||
--united_with_shadow;
|
||||
if (united_with_shadow == 0) { --united_with_shadow; /* → -1 */ ... }
|
||||
}
|
||||
```
|
||||
|
||||
`united_with_shadow` живёт как счётчик, потом как «уже слились» (−1).
|
||||
На него смотрят `check_shadow` и `check_can_guard_see_kid`.
|
||||
|
||||
### 1.5 Луч видимости (`check_can_guard_see_kid`, seg003:702)
|
||||
|
||||
```c
|
||||
if ((Guard.charid != charid_1_shadow || current_level == 12) && ...
|
||||
```
|
||||
|
||||
У нас (`guards.c:103`) условие про тень **нужно сверить**: на уровне 12 тень
|
||||
ОБЯЗАНА быть видимой (иначе Кид не достанет меч и бой не начнётся).
|
||||
|
||||
### 1.6 Меч исчезает (`sword_disappears`, seg002:0536)
|
||||
|
||||
```c
|
||||
if (current_level == 12 && Char.room == 18) {
|
||||
get_tile(15, 1, 0);
|
||||
curr_room_tiles[curr_tilepos] = tiles_1_floor;
|
||||
curr_room_modif[curr_tilepos] = 0;
|
||||
}
|
||||
```
|
||||
|
||||
Срабатывает при уходе Кида ВПРАВО из комнаты 18 (`leave_room`, ветка 1).
|
||||
У нас есть `pop_level_set_tile` — порт на пять строк.
|
||||
|
||||
### 1.7 Переход 12 → 13: НЕТ двери уровня, есть «бесшовный выход»
|
||||
|
||||
Проверено по данным (`res2012.bin` / `res2013.bin`) — портала действительно
|
||||
нет, уровень кончается **фактом попадания в комнату**:
|
||||
|
||||
```c
|
||||
// play_level_2, seg000:900
|
||||
} else if (custom->tbl_seamless_exit[current_level] >= 0) {
|
||||
if (Kid.room == /*23*/ custom->tbl_seamless_exit[current_level]) {
|
||||
++next_level;
|
||||
stop_sounds();
|
||||
seamless = 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`tbl_seamless_exit[12] = 23` (пара «уровень, комната» читается из оригинального
|
||||
`PRINCE.EXE`, options.c:724). Дальше геометрия складывается так:
|
||||
|
||||
| | комната | ряд 1 | что происходит |
|
||||
|---|---|---|---|
|
||||
| ур. 12 | 13 | `floor bigpil empty empty wall wall …` | Кид бежит ВЛЕВО с колонки 0 |
|
||||
| ур. 12 | 23 | `empty ×6, floor floor floor floor` | попал сюда → **уровень сменился** |
|
||||
| ур. 13 | 23 | `floor bigpil floor ×6 bigpil floor` | старт: ряд 1, кол 9, `seq_84_run` |
|
||||
|
||||
Связи: `комната 13.left = 23`, и в комнату 23 больше ниоткуда не войти.
|
||||
Номер комнаты у обоих уровней **один и тот же (23)**, стартовая позиция
|
||||
уровня 13 — `BP_START = 23, поз 19 (ряд 1, кол 9), dir 0`, а
|
||||
`tbl_entry_pose[13] = 2` даёт «вбегающий» вход (`seq_84_run`, seg003:172).
|
||||
То есть Кид вбегает в комнату слева-направо… нет, `Char.direction =
|
||||
~level.start_dir` — влево, тем же ходом, каким выбежал из уровня 12. Швов не
|
||||
видно.
|
||||
|
||||
Что делает флаг `seamless` (всего два места, оба косметические):
|
||||
|
||||
* `start_level`, seg003:158 — **НЕ сбрасывает HP**: Кид уносит на уровень 13
|
||||
то здоровье, с которым добежал;
|
||||
* `show_level`, seg008:1861/1870 — не показывает заставку «LEVEL 13» и тут же
|
||||
гасит флаг.
|
||||
|
||||
Двери уровня при этом на карте есть, но не при делах: у уровня 12 она одна
|
||||
(комната 3, tilepos 23) — это ВХОД (стартовая комната 12-го — 3), а у уровня
|
||||
13 их две (комната 3 tilepos 13 и комната 5 tilepos 24) — это выходы,
|
||||
открываемые кнопкой из `Jaffar_exit`.
|
||||
|
||||
**Что это значит для нас.** Наш переход уровня сейчас идёт только через
|
||||
`SEQ_END_LEVEL` в двери. Для 12-го нужен второй триггер — проверка в главном
|
||||
цикле «`pop_current_level == 12 && cur_room == 23` → `pop_next_level++`» плюс
|
||||
флаг `seamless`, который пропустит сброс HP в `pop_start_level`. Обе правки
|
||||
маленькие и локальные.
|
||||
|
||||
---
|
||||
|
||||
## 2. Уровень 13 — Джафар
|
||||
|
||||
### 2.1 Кто такой Джафар
|
||||
|
||||
`tbl_guard_type[13] = 3` → `VIZIER.DAT` (у нас в `pop_level_cold.c` уже 3).
|
||||
`tbl_guard_hp[13] = 6`. Отдельного ИИ у него нет:
|
||||
|
||||
```c
|
||||
void autocontrol_Jaffar() { autocontrol_guard(); } // seg002:0697
|
||||
```
|
||||
|
||||
То есть **бой с Джафаром — обычный бой стражи**, отличаются только спрайты,
|
||||
HP и три спецсобытия ниже. Это хорошая новость: боёвка у нас есть.
|
||||
|
||||
### 2.2 Встреча (`meet_Jaffar`, seg002:0544)
|
||||
|
||||
```c
|
||||
if (current_level == 13 && leveldoor_open == 0 && Char.room == 3) {
|
||||
play_sound(sound_29_meet_Jaffar);
|
||||
guard_notice_timer = 28; // Джафар ждёт 28/12 ≈ 2.33 с
|
||||
}
|
||||
```
|
||||
|
||||
Срабатывает при уходе Кида ВПРАВО (`leave_room`, ветка 1).
|
||||
Пара к нему — в `autocontrol_guard_inactive` (seg002:0734), она у нас уже
|
||||
портирована (`guards.c:401`), но **без условия по уровню**:
|
||||
|
||||
```c
|
||||
if (can_guard_see_kid) {
|
||||
if (current_level != 13 || guard_notice_timer == 0) move_down_forw();
|
||||
}
|
||||
```
|
||||
|
||||
**Что добавить:** глобал `guard_notice_timer` + его тик в `do_timers`
|
||||
(seg003:509) + оба условия.
|
||||
|
||||
### 2.3 Победа (`on_guard_killed`, seg006:1929)
|
||||
|
||||
```c
|
||||
} else if (current_level == 13) {
|
||||
flash_color = color_15_brightwhite; flash_time = 18;
|
||||
is_show_time = 1;
|
||||
leveldoor_open = 2; // ← ключ к выходу
|
||||
play_sound(sound_43_victory_Jaffar);
|
||||
}
|
||||
```
|
||||
|
||||
и парная `Jaffar_exit` (seg002:0517), срабатывающая при уходе Кида ВЛЕВО:
|
||||
|
||||
```c
|
||||
if (leveldoor_open == 2) { get_tile(24, 0, 0); trigger_button(0, 0, -1); }
|
||||
```
|
||||
|
||||
То есть смерть Джафара не открывает дверь сама — она ставит флаг, а дверь
|
||||
открывается кнопкой, «нажатой» при уходе влево. `trigger_button` у нас есть.
|
||||
|
||||
### 2.4 Падающие плиты (`check_fall_flo`, seg000:1319)
|
||||
|
||||
```c
|
||||
if (current_level == 13 && (drawn_room == 23 || drawn_room == 16)) {
|
||||
curr_room = room_A; // комната СВЕРХУ
|
||||
for (curr_tilepos = 22; curr_tilepos <= 27; ++curr_tilepos)
|
||||
make_loose_fall(-(prandom(0xFF) & 0x0F)); // ОТРИЦАТЕЛЬНЫЙ модификатор
|
||||
}
|
||||
```
|
||||
|
||||
Вот это и есть «плиты появляются»: при входе в комнаты 23/16 шесть плит ряда 2
|
||||
комнаты СВЕРХУ получают отрицательную фазу — то есть отложенный старт, и
|
||||
сыплются на Кида вразнобой. Зовётся из `check_the_end` при смене комнаты.
|
||||
|
||||
**Три спецкейса уровня 13 в loose-механике**, без них это не работает:
|
||||
|
||||
| место | что | зачем |
|
||||
|---|---|---|
|
||||
| `animate_loose`, seg007:823 | при `modif & 0x80` НЕ останавливать тряску | иначе отрицательная фаза не досчитает до нуля и плита не упадёт |
|
||||
| `loose_make_shake`, seg007:949 | на уровне 13 сотрясение НЕ трясёт плиты | иначе отложенные плиты сбрасываются в 0x80 |
|
||||
| `fell_on_your_head`, seg007:1218 | плита бьёт и в БЕГЕ (кадры 5..14) | на прочих уровнях бегущего не задевает |
|
||||
|
||||
У нас первый пункт критичен: `pop_loose_tick` трактует бит 7 как «тряска» и
|
||||
на `>= 0x84` гасит фазу — отрицательный старт умрёт, не начавшись. Заодно
|
||||
это ровно та же ветка, что мы правили сегодня в
|
||||
[`LOOSE-ROOM-CHANGE`](../roomtest/BUGS_OPEN.md#loose-room-change), так что
|
||||
код на виду.
|
||||
|
||||
### 2.5 Поза входа
|
||||
|
||||
`tbl_entry_pose[13] = 2` — у нас в таблице уже есть; проверить, что режим 2
|
||||
(`seg003:172`) реализован.
|
||||
|
||||
---
|
||||
|
||||
## 3. Уровень 14 — принцесса и конец игры
|
||||
|
||||
Боя нет вовсе (`tbl_guard_type[14] = -1`). Всё сводится к одному событию:
|
||||
|
||||
```c
|
||||
// check_the_end, seg000:1299
|
||||
if (current_level == 14 && drawn_room == 5) end_sequence();
|
||||
```
|
||||
|
||||
`end_sequence` (seg001:573) → `end_sequence_anim` (seg001:332): катсцена
|
||||
«Кид добежал до принцессы» — обнимаются, появляется мышь, затухание, Hall of
|
||||
Fame. Персонажи там играются ТЕМ ЖЕ интерпретатором `seqtbl`
|
||||
(`seq_108_princess_turn_and_hug`, `seq_101_mouse_stands_up`), то есть движок
|
||||
у нас уже подходит — нужны спрайты принцессы (`PRINCESS.DAT`) и раскадровка.
|
||||
|
||||
**Оценка:** это не игровая механика, а ролик. Логично делать вместе с интро
|
||||
и межуровневыми вставками — отдельным банком, как и договаривались
|
||||
(см. memory `pop_banking_architecture`). На проходимость игры не влияет:
|
||||
достаточно довести Кида до комнаты 5 и показать заглушку.
|
||||
|
||||
---
|
||||
|
||||
## 4. Уровень 15 — «уровень зелий» (защита от копирования)
|
||||
|
||||
Не часть сюжета. Это экран проверки подлинности из оригинала: после
|
||||
уровня `copyprot_level` игра подменяет номер на 15, показывает комнату с
|
||||
**14 зельями, на которых нарисованы буквы**, и требует выпить нужное.
|
||||
|
||||
Механика (всё под `USE_COPYPROT` в SDLPoP):
|
||||
|
||||
| место | что делает |
|
||||
|---|---|
|
||||
| `play_level`, seg003:53 | `level_number == copyprot_level` → грузим 15 |
|
||||
| `redraw_screen`, seg003:273 | поверх зелий рисуются БУКВЫ (`copyprot_letter`) |
|
||||
| `load_alter_mod`, seg008:1204 | одно зелье в комнате делается «открытым» (тип 6) |
|
||||
| `animate_potion`, seg007:259 | на уровне 15 своя ветка перерисовки |
|
||||
| `up_pressed`/`do_pickup`, seg005:657 | выпитое зелье убирает букву из таблицы |
|
||||
| `seq` эффект зелья, seg006:1896 | синие зелья на уровне 15 отнимают ПОЛОВИНУ HP |
|
||||
| выход, seg000:700 | из 15 возвращаемся в `copyprot_level` |
|
||||
|
||||
**Рекомендация: не портировать.** Это анти-пиратский экран 1989 года,
|
||||
требующий книжки-манускрипта; SDLPoP держит его выключенным по умолчанию
|
||||
(`enable_copyprot`). Единственное, что стоит взять — **половинный урон
|
||||
синего зелья**, если вдруг захочется полной совместимости; остальное только
|
||||
съест банк. Если решим делать — это отдельная фича «уровень 15», а не часть
|
||||
основного прохождения.
|
||||
|
||||
---
|
||||
|
||||
## 5. Уровень 0 — демо-уровень (аттракт)
|
||||
|
||||
`res2000.bin` у нас распакован. Это тот самый ролик, который крутится на
|
||||
титульном экране: Кид сам бежит, дерётся со стражем и убегает.
|
||||
|
||||
Как устроено:
|
||||
|
||||
| место | что |
|
||||
|---|---|
|
||||
| `do_demo`, seg006:1409 | на уровне 0 вместо чтения клавиатуры зовётся `do_demo()` + `control()` |
|
||||
| `autocontrol_kid`, seg002:702 | Кид управляется тем же `autocontrol_guard` |
|
||||
| `do_auto_moves`, seg002:1089 | проигрыватель ЗАПИСИ ходов: таблица `(time, move)` |
|
||||
| `on_guard_killed`, seg006:1928 | на уровне 0 после убийства стража Кид убегает (`checkpoint = 1`, сброс демо) |
|
||||
| `demo_index` / `demo_time` | позиция в записи; у нас уже объявлены в `guards.c:267` |
|
||||
|
||||
**Хорошая новость:** движок автодвижений (`do_auto_moves`) у нас уже есть —
|
||||
он нужен был тени на уровнях 4/5/6, и `demo_time`/`demo_index` объявлены там
|
||||
же. То есть демо-уровень — это в основном таблица ходов + ветка «Кидом
|
||||
управляет ИИ» в `pop_ctrl`.
|
||||
|
||||
**Когда делать:** вместе с интро/титульным экраном, не раньше. На
|
||||
прохождение не влияет.
|
||||
|
||||
---
|
||||
|
||||
## 6. Порядок работ
|
||||
|
||||
Порядок задан пользователем 2026-08-13: **строго по номерам уровней**, а не
|
||||
по дешевизне кода — уровень 13 не имеет смысла раньше, чем на него можно
|
||||
попасть.
|
||||
|
||||
1. ✅ **Уровень 12** — тень: `init_shad_12`, `autocontrol_shadow_level12`,
|
||||
`united_with_shadow`, общий урон, «убил тень — убил себя»,
|
||||
`sword_disappears`, появление плит. Сделано 2026-08-13, хост-набор
|
||||
`roomtest/tests-host/t_shadow.c` (45 проверок); живой проверки в MAME
|
||||
ещё не было — доска [`L12-SHADOW`](../roomtest/TASKS_OPEN.md#l12-shadow).
|
||||
2. ✅ **Переход 12 → 13** (§1.7) — room-триггер вместо двери и флаг
|
||||
`pop_seamless` (не сбрасывать HP). Сделано там же.
|
||||
3. ✅ **Уровень 13** — Джафар: `guard_notice_timer`,
|
||||
`on_guard_killed`/`Jaffar_exit`, три спецкейса loose (§2.4) и
|
||||
`check_fall_flo`. Плюс СПРАЙТЫ визиря (VIZIER.DAT) — без них он
|
||||
рисовался обычным стражем. Сделано 2026-08-13, хост-набор
|
||||
`roomtest/tests-host/t_jaffar.c` (44 проверки); живой проверки в MAME ещё
|
||||
не было — доска [`L13-JAFFAR`](../roomtest/TASKS_OPEN.md#l13-jaffar).
|
||||
4. **Уровень 14** — довести до комнаты 5 и поставить заглушку вместо ролика;
|
||||
сам ролик — в общую задачу «катсцены».
|
||||
5. **Уровень 0 и 15** — отложить: аттракт и защита от копирования на
|
||||
прохождение не влияют.
|
||||
|
||||
---
|
||||
|
||||
## 7. Сверка констант с ОРИГИНАЛЬНЫМИ данными (сделана 2026-08-13)
|
||||
|
||||
Всё ниже снято скриптом прямо с `../SDLPoP/data/LEVELS/res2013.bin` и
|
||||
`res2012.bin` — не из головы и не из констант SDLPoP.
|
||||
|
||||
### 7.1 Падающие плиты уровня 13 — константы сходятся ТОЧНО
|
||||
|
||||
Комнаты, у которых в ряду 2 вообще есть loose:
|
||||
|
||||
| комната | loose в колонках | комната снизу |
|
||||
|---|---|---|
|
||||
| 17 | 2, 3, 4, 5, 6, 7 | **23** |
|
||||
| 1 | 2, 3, 5, 6, 7 | **16** |
|
||||
| 14 | 2 | 24 |
|
||||
|
||||
Связи: `комната 23: up = 17`, `комната 16: up = 1`. То есть
|
||||
`loose_tiles_room_1 = 23` и `room_2 = 16` — это ровно те две комнаты, над
|
||||
которыми лежит ПОЛНАЯ гряда плит, а `first_tile = 22, last_tile = 27` — ровно
|
||||
ряд 2, колонки 2..7:
|
||||
|
||||
```
|
||||
комната 23 → сверху 17: 22:LOOSE 23:LOOSE 24:LOOSE 25:LOOSE 26:LOOSE 27:LOOSE [6/6]
|
||||
комната 16 → сверху 1: 22:LOOSE 23:LOOSE 24:empty 25:LOOSE 26:LOOSE 27:LOOSE [5/6]
|
||||
```
|
||||
|
||||
Два вывода:
|
||||
|
||||
* диапазон 22..27 — **надмножество**: в комнате 1 тайл 24 пустой, и
|
||||
`make_loose_fall` его молча пропустит (проверяет тип тайла). Копировать
|
||||
константы можно как есть;
|
||||
* пара «комната 14 → 24» НЕ входит в спецсобытие намеренно — одна плита, это
|
||||
обычный loose.
|
||||
|
||||
**Важное следствие, которого не было в плане:** стартовая комната уровня 13 —
|
||||
**23** (`BP_START = 23`, поз 19, dir 0), а `check_fall_flo` зовётся из
|
||||
`draw_level_first` → `check_the_end` (seg003:217). Значит плиты начинают
|
||||
сыпаться СРАЗУ при входе на уровень, это его первый кадр, а не событие
|
||||
где-то в середине.
|
||||
|
||||
### 7.2 Джафар — один страж со skill 9
|
||||
|
||||
В `res2013.bin` заполнен ровно один слот стража:
|
||||
|
||||
| комната | тайл | ряд, кол | направление | skill |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 7 | 0, 7 | 255 (влево) | **9** |
|
||||
|
||||
И это сходится с `meet_Jaffar`: событие срабатывает, когда Кид уходит ВПРАВО
|
||||
из комнаты 3, а `комната 3: right = 1` — то есть ровно туда, где стоит
|
||||
Джафар. Skill 9 у нас поддержан: `NUM_GUARD_SKILLS = 12`, все шесть таблиц
|
||||
вероятностей (`guards.c:411..415`) имеют индекс 9. HP = `tbl_guard_hp[13] = 6`
|
||||
(в `pop_level_cold.c` уже стоит).
|
||||
|
||||
`Jaffar_exit` тоже проверен: **комната 24, tilepos 0 = `opener`** (кнопка),
|
||||
модификатор 0 — то есть `trigger_button(0,0,-1)` жмёт реальную кнопку, а не
|
||||
пустой тайл.
|
||||
|
||||
### 7.3 Уровень 12 — меч на месте
|
||||
|
||||
`комната 15, tilepos 1 = SWORD` — условие подъёма тени
|
||||
(`get_tile(15,1,0) == tiles_22_sword`) на наших данных выполняется.
|
||||
`комната 18: right = 19` — `sword_disappears` срабатывает при уходе вправо.
|
||||
|
||||
Комната 15 целиком (по ней видно всю сцену встречи с тенью):
|
||||
|
||||
```
|
||||
ряд 0: floor SWORD floor torch torch LOOSE LOOSE floor empty empty
|
||||
ряд 1: wall wall wall empty empty empty empty wall floor floor
|
||||
ряд 2: wall opener pillar empty empty empty empty wall pillar empty
|
||||
```
|
||||
|
||||
### 7.4 `pop_loose_tick` действительно убьёт отрицательную фазу
|
||||
|
||||
Подтверждено чтением кода: `m = ++pop_loose_modif[pos]; if (m & 0x80) { if (m
|
||||
>= 0x84) → сброс в 0 }`. Для стартового `0xF0..0xFF` первый же тик даёт
|
||||
`m >= 0x84` → фаза обнуляется, плита не падает. **Это и есть та правка №1 из
|
||||
таблицы §2.4**, и она обязательна.
|
||||
|
||||
### 7.5 `init_shad_12` и байтовое переполнение `y`
|
||||
|
||||
`init_shad_12 = {0x0F, 0x51, 0xE8, 0, 0, 0, 0, 0}` → frame 15, x 81,
|
||||
**y 232**, вправо, колонка 0, ряд 0, action 0 + `seq 7 (fall)`. Поле `y` в
|
||||
`char_type` — `byte` (беззнаковое), то есть 232 лежит НИЖЕ поля (192), и
|
||||
падение уводит его дальше с переполнением байта: тень «падает» из-под экрана и
|
||||
появляется сверху. У нас `pop_char_t.y` тоже `uint8_t` — поведение
|
||||
переносится без правок; при портировании просто не «чинить» это как ошибку.
|
||||
|
||||
Отдельно про ряд: `curr_row` берётся ИЗ ТАБЛИЦЫ и равен **0**, хотя
|
||||
`pop_y_to_row(232)` дал бы −1. `do_init_shad` копирует семь полей как есть и
|
||||
ряд не пересчитывает, так что «согласовать» их — значит разойтись с
|
||||
оригиналом. Закреплено тестом `shadow12_rises_when_sword_gone`.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,7 @@
|
||||
| ID | что | тип | статус |
|
||||
|----|-----|-----|--------|
|
||||
| [SPRITE-ZERO-W0](#sprite-zero-w0) | кадр персонажа изредка отдаёт спрайт 0x0 (залипание уже вылечено) | **редкий** | открыт: нужна трасса маппинга W0 |
|
||||
| [GATE-FORE-KID](#gate-fore-kid) | Кид в проёме ворот виден поверх решётки | окклюзия | открыт: портирован только шовный случай |
|
||||
| [GATE-FORE-KID](#gate-fore-kid) | Кид в проёме ворот виден поверх решётки | окклюзия | **фикс есть, ждёт проверки в MAME** (2026-08-13) |
|
||||
| [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить |
|
||||
| [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт |
|
||||
| [TORCH-ANIM-RIGHT](#torch-anim-right) | под запечённым пламенем застывают не только челюсти чомпера | **низкий** | открыт: на уровнях 1-4 такого соседства нет |
|
||||
@@ -23,6 +23,13 @@
|
||||
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
|
||||
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
|
||||
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
|
||||
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** |
|
||||
| [LOOSE-ROOM-CHANGE](#loose-room-change) | расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату | порт | закрыт 2026-08-13, кроме [LOOSE-SEAM-ANIM](#loose-seam-anim) |
|
||||
| [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** |
|
||||
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
|
||||
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
|
||||
| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки |
|
||||
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
||||
|
||||
---
|
||||
|
||||
@@ -427,7 +434,31 @@ for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row)
|
||||
читать её из MAME. Симптом редкий (пользователь поймал дважды).
|
||||
|
||||
<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, верхние ворота):
|
||||
Кид стоит на тайле ворот, а решётка рисуется ПОД ним — он виден целиком,
|
||||
@@ -465,3 +496,319 @@ if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
|
||||
**Проверять:** уровень 5, комната 24 — встать в проём верхних ворот
|
||||
(колонка 1 ряда 0) при частично поднятой решётке; прутья должны
|
||||
перекрывать Кида. Тот же случай — любые ворота на уровнях 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`
|
||||
живёт в резиденте.
|
||||
|
||||
@@ -94,8 +94,16 @@ SKEL_DIR := $(CURDIR)/../poc/res/skel
|
||||
# страж (tbl_guard_type[9] = 0). См. pop_pack_guard.py.
|
||||
SKEL_DATA := $(SKEL_DIR)/g0.atl $(SKEL_DIR)/g1.atl $(SKEL_DIR)/g2.atl \
|
||||
$(SKEL_DIR)/g3.atl
|
||||
# Джафар — тоже отдельный набор (VIZIER.DAT): его грузит уровень 13
|
||||
# (tbl_guard_type == 3). Кадров столько же, сколько у стража (34), но
|
||||
# палитра своя, из файла набора: curr_guard_color для не-стражей = 0.
|
||||
# Зеркального набора нет по той же причине, что у скелета.
|
||||
VIZIER_DIR := $(CURDIR)/../poc/res/vizier
|
||||
VIZIER_DATA := $(VIZIER_DIR)/g0.atl $(VIZIER_DIR)/g1.atl $(VIZIER_DIR)/g2.atl \
|
||||
$(VIZIER_DIR)/g3.atl $(VIZIER_DIR)/g4.atl
|
||||
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) $(LVL_DATA)
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
|
||||
$(VIZIER_DATA) $(LVL_DATA)
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
||||
@@ -111,6 +119,7 @@ hdd: $(EXAMPLE).exe
|
||||
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
||||
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
||||
$(foreach f,$(SKEL_DATA),SKEL:$(f)) \
|
||||
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
|
||||
$(foreach f,$(LVL_DATA),LEVELS:$(f))
|
||||
|
||||
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
||||
@@ -122,6 +131,9 @@ $(GUARD_DATA): $(TC)/pop_pack_guard.py
|
||||
$(SKEL_DATA): $(TC)/pop_pack_guard.py
|
||||
cd $(TC) && python3 pop_pack_guard.py SKEL
|
||||
|
||||
$(VIZIER_DATA): $(TC)/pop_pack_guard.py
|
||||
cd $(TC) && python3 pop_pack_guard.py VIZIER
|
||||
|
||||
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
||||
cd $(TC) && python3 pop_pack_kid.py
|
||||
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
|
||||
|
||||
@@ -231,6 +231,151 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
|
||||
|
||||
## 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)
|
||||
|
||||
> **Статус 2026-08-12: код написан, ждёт живой проверки в MAME.** Сделаны все
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
|
||||
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
|
||||
|
||||
#define TILE_FLOOR 1
|
||||
#define TILE_SPIKE 2
|
||||
#define TILE_GATE 4
|
||||
#define TILE_DOORTOP_FLOOR 7
|
||||
@@ -270,18 +271,28 @@ static uint8_t demo_index; /* текущая запись
|
||||
#define SHADOW_SKILL 3
|
||||
#define SHADOW_HP 4
|
||||
#define SEQ_2_STAND 2
|
||||
#define SEQ_7_FALL 7
|
||||
#define TILE_POTION 10
|
||||
#define TILE_SWORD 22
|
||||
|
||||
/* Таблицы init_shad_N (data:0EEA/0EF2) — ПЕРВЫЕ СЕМЬ полей Char в том же
|
||||
* порядке, в каком их копирует оригинал (`memcpy(&Char, source, 7)`):
|
||||
* frame, x, y, direction, curr_col, curr_row, action.
|
||||
* 5: x 55, y 55, вправо, колонка −1 (ещё за левым краем экрана), ряд 0;
|
||||
* 6: x 81, y 118, вправо, колонка 0, ряд 1 — тень стоит на границе
|
||||
* (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту. */
|
||||
static const uint8_t init_shad_5[7] = { 0x0F, 0x37, 0x37, 0, 0xFF, 0, 0 };
|
||||
static const uint8_t init_shad_6[7] = { 0x0F, 0x51, 0x76, 0, 0x00, 1, 0 };
|
||||
* (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту;
|
||||
* 12: x 81, y 232, вправо, колонка 0, ряд 0 — и это НЕ ошибка данных: 232
|
||||
* лежит НИЖЕ поля (192), поле y беззнаковое, и тень с seq_7 «падает»
|
||||
* из-под комнаты, переполняя байт, то есть влетает в кадр сверху.
|
||||
* Ряд при этом берётся ИЗ ТАБЛИЦЫ (0), а не пересчитывается из y
|
||||
* (иначе был бы −1): семь полей копируются как есть, и «поправить»
|
||||
* их согласованность нельзя (см. ../docs/levels_12_15_plan.md §7.5). */
|
||||
static const uint8_t init_shad_5[7] = { 0x0F, 0x37, 0x37, 0, 0xFF, 0, 0 };
|
||||
static const uint8_t init_shad_6[7] = { 0x0F, 0x51, 0x76, 0, 0x00, 1, 0 };
|
||||
static const uint8_t init_shad_12[7] = { 0x0F, 0x51, 0xE8, 0, 0x00, 0, 0 };
|
||||
|
||||
static void do_init_shad(const uint8_t *tbl, uint8_t room)
|
||||
/* seq — поза, с которой тень входит в сцену: 5/6 встают в стойку, 12 падает. */
|
||||
static void do_init_shad(const uint8_t *tbl, uint8_t room, uint8_t seq)
|
||||
{
|
||||
pop_loadshad();
|
||||
Char.frame = tbl[0];
|
||||
@@ -296,7 +307,7 @@ static void do_init_shad(const uint8_t *tbl, uint8_t room)
|
||||
Char.alive = Kid.alive; /* наследование от Кида, см. шапку */
|
||||
Char.sword = Kid.sword;
|
||||
Char.fall_x = Char.fall_y = 0;
|
||||
pop_char_set_seq(SEQ_2_STAND);
|
||||
pop_char_set_seq(seq);
|
||||
pop_saveshad();
|
||||
|
||||
demo_time = 0;
|
||||
@@ -309,6 +320,25 @@ static void do_init_shad(const uint8_t *tbl, uint8_t room)
|
||||
|
||||
uint8_t pop_check_shadow(uint8_t room) __banked
|
||||
{
|
||||
/* Уровень 12 (seg002:0070) — встреча с тенью в комнате 15. Три отличия
|
||||
* от 5/6, и все три существенные:
|
||||
* — событие одноразовое: после слияния (pop_united_shadow != 0) тень
|
||||
* больше не поднимается ни при каком входе в комнату;
|
||||
* — условие на тайл ОБРАТНОЕ уровню 5: там тень поднимается, ПОКА
|
||||
* зелье на месте, здесь — только когда меча (15,1,0) уже НЕТ, то
|
||||
* есть Кид его подобрал. Меч на месте — return 0, и это не «ничего
|
||||
* не делать»: стража в комнате 15 в данных нет, так что вызывающий
|
||||
* спокойно идёт в pop_guard_enter и убирает слот;
|
||||
* — поза входа не стойка, а ПАДЕНИЕ (seq 7).
|
||||
* pop_shadow_init сбрасываем здесь же: тень начинает «ждать Кида»
|
||||
* заново при каждом подъёме (см. autocontrol_shadow_level12). */
|
||||
if (pop_current_level == SHADOW_FIGHT_LEVEL) {
|
||||
if (pop_united_shadow || room != SHADOW_FIGHT_ROOM) return 0;
|
||||
if (pop_level_tile(room, SHADOW_SWORD_TILEPOS) == TILE_SWORD) return 0;
|
||||
pop_shadow_init = 0;
|
||||
do_init_shad(init_shad_12, room, SEQ_7_FALL);
|
||||
return 1;
|
||||
}
|
||||
/* Уровень 6 (seg002:0090): условий на содержимое комнаты нет вовсе —
|
||||
* тень встаёт при КАЖДОМ входе в комнату 1. Оригинал здесь ещё играет
|
||||
* sound_25_presentation один раз, отмечая это `leveldoor_open = 0x4D`
|
||||
@@ -316,7 +346,7 @@ uint8_t pop_check_shadow(uint8_t room) __banked
|
||||
* переменную портить незачем — TODO вместе со звуком. */
|
||||
if (pop_current_level == SHADOW_STEP_LEVEL) {
|
||||
if (room != SHADOW_STEP_ROOM) return 0;
|
||||
do_init_shad(init_shad_6, room);
|
||||
do_init_shad(init_shad_6, room, SEQ_2_STAND);
|
||||
return 1;
|
||||
}
|
||||
if (pop_current_level != SHADOW_STEAL_LEVEL) return 0;
|
||||
@@ -324,10 +354,108 @@ uint8_t pop_check_shadow(uint8_t room) __banked
|
||||
/* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража
|
||||
* комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */
|
||||
if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1;
|
||||
do_init_shad(init_shad_5, room);
|
||||
do_init_shad(init_shad_5, room, SEQ_2_STAND);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* sword_disappears (seg002:0536) — спецсобытие уровня 12: меч, лежащий в
|
||||
* комнате 15, пропадает с карты, как только Кид ушёл ВПРАВО из комнаты 18.
|
||||
* Смысл событийный: дальше по уровню возврата за мечом уже нет, и сцена с
|
||||
* тенью обязана быть однозначной — либо Кид пришёл к ней вооружённым, либо
|
||||
* нет. Модификатор гасим вместе с тайлом (как SDLPoP): ненулевой modif на
|
||||
* полу рисует «фальшивые» узоры.
|
||||
*
|
||||
* Звать при уходе ВПРАВО, ДО смены комнаты, — там же, где level3_set_chkp
|
||||
* (оригинал держит оба в leave_room). */
|
||||
void pop_sword_disappears(void) __banked
|
||||
{
|
||||
uint8_t *m;
|
||||
if (pop_current_level != SHADOW_FIGHT_LEVEL) return;
|
||||
if (Kid.room != SWORD_GONE_ROOM) return;
|
||||
pop_level_set_tile(SHADOW_FIGHT_ROOM, SHADOW_SWORD_TILEPOS, TILE_FLOOR);
|
||||
m = pop_trob_modif(SHADOW_FIGHT_ROOM);
|
||||
if (m) m[SHADOW_SWORD_TILEPOS] = 0;
|
||||
}
|
||||
|
||||
/* meet_Jaffar (seg002:0544) — спецсобытие уровня 13: Кид уходит ВПРАВО из
|
||||
* комнаты 3 и попадает прямо к Джафару. Всё событие — фора: пока
|
||||
* pop_guard_notice_timer идёт, Джафар не встаёт в боевую стойку
|
||||
* (autocontrol_guard_inactive), то есть у Кида есть ~2 секунды достать меч.
|
||||
* В оригинале здесь же играет sound_29_meet_Jaffar — звука у нас нет.
|
||||
*
|
||||
* `pop_leveldoor_open == 0` в условии не косметика: вернувшись к Джафару
|
||||
* после его смерти (дверь уже открыта), форы быть не должно.
|
||||
*
|
||||
* Звать при уходе ВПРАВО, ДО смены комнаты, — рядом с pop_sword_disappears
|
||||
* (оригинал держит оба в одной ветке leave_room). */
|
||||
void pop_meet_jaffar(void) __banked
|
||||
{
|
||||
if (pop_current_level != JAFFAR_LEVEL) return;
|
||||
if (pop_leveldoor_open != 0) return;
|
||||
if (Kid.room != JAFFAR_MEET_ROOM) return;
|
||||
pop_guard_notice_timer = JAFFAR_NOTICE_TIME;
|
||||
}
|
||||
|
||||
/* Jaffar_exit (seg002:0517) — парная половина победы: смерть Джафара сама
|
||||
* дверь НЕ открывает, она лишь ставит pop_leveldoor_open = 2. Дверь
|
||||
* открывает КНОПКА комнаты 24 (tilepos 0 — настоящий opener, проверено по
|
||||
* res2013.bin), «нажимаемая» в момент ухода Кида ВЛЕВО.
|
||||
*
|
||||
* Звать при уходе ВЛЕВО, ДО смены комнаты, — рядом с чекпойнтом уровня 3. */
|
||||
void pop_jaffar_exit(void) __banked
|
||||
{
|
||||
uint8_t *m;
|
||||
if (pop_leveldoor_open != 2) return;
|
||||
m = pop_trob_modif(JAFFAR_EXIT_ROOM);
|
||||
pop_trigger_button(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS,
|
||||
pop_level_tile(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS),
|
||||
m ? m[JAFFAR_EXIT_TILEPOS] : 0);
|
||||
}
|
||||
|
||||
/* on_guard_killed (seg006:1927) — соперник только что потерял последнее HP.
|
||||
* Уровень 13 — единственный, где у этого есть последствия помимо звука:
|
||||
* белая вспышка и pop_leveldoor_open = 2, то есть «выход разрешён» (сама
|
||||
* дверь откроется кнопкой из pop_jaffar_exit).
|
||||
*
|
||||
* Ветку `is_show_time = 1` не портируем: это показ ОСТАВШЕГОСЯ ВРЕМЕНИ, а
|
||||
* часов на 60 минут у нас пока нет вовсе — появятся вместе с таймером игры.
|
||||
* Демо-уровень (0) сюда тоже не входит: его ветка — часть аттракта. */
|
||||
static void on_guard_killed(void)
|
||||
{
|
||||
if (pop_current_level != JAFFAR_LEVEL) return;
|
||||
pop_flash_color = POP_FLASH_WHITE;
|
||||
pop_flash_time = JAFFAR_FLASH_TIME;
|
||||
pop_leveldoor_open = 2;
|
||||
}
|
||||
|
||||
/* check_killed_shadow (seg006:2033) — «убил тень = убил себя». На уровне 12
|
||||
* Кид и тень связаны насмерть: как только один из них перестал быть живым
|
||||
* (alive >= 0), второму отсчитывается 100 урона, то есть гарантированная
|
||||
* смерть, и обоих накрывает белая вспышка.
|
||||
*
|
||||
* Условие оригинала `(Char.charid | Opp.charid) == charid_1_shadow` читается
|
||||
* так: в паре ровно Кид (0) и тень (1) — со стражем или скелетом связи нет.
|
||||
* Оригинал зовёт это ДВАЖДЫ за кадр (из тика Кида и из тика соперника, с
|
||||
* уже переставленными окнами); нам хватает одного вызова на кадр с явным
|
||||
* разбором обеих сторон — окна Char/Opp здесь не нужны, кроме как для
|
||||
* маршрутизации урона внутри pop_take_hp. */
|
||||
void pop_check_killed_shadow(void) __banked
|
||||
{
|
||||
if (pop_current_level != SHADOW_FIGHT_LEVEL) return;
|
||||
if ((uint8_t)(Kid.charid | Guard.charid) != CHARID_1_SHADOW) return;
|
||||
if (Kid.alive < 0 && Guard.alive >= 0) {
|
||||
pop_flash_color = POP_FLASH_WHITE;
|
||||
pop_flash_time = 5;
|
||||
pop_loadkid();
|
||||
pop_take_hp(100);
|
||||
} else if (Guard.alive < 0 && Kid.alive >= 0) {
|
||||
pop_flash_color = POP_FLASH_WHITE;
|
||||
pop_flash_time = 5;
|
||||
pop_loadshad();
|
||||
pop_take_hp(100);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
|
||||
static void move_0_nothing(void)
|
||||
{
|
||||
@@ -399,8 +527,11 @@ static void autocontrol_guard_inactive(void)
|
||||
}
|
||||
}
|
||||
if (can_guard_see_kid) {
|
||||
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
|
||||
move_down_forw();
|
||||
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч).
|
||||
* Уровень 13: пока идёт фора после встречи (pop_meet_jaffar), Джафар
|
||||
* стоит и ждёт — это его единственное отличие от прочих стражей. */
|
||||
if (pop_current_level != JAFFAR_LEVEL || pop_guard_notice_timer == 0)
|
||||
move_down_forw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +817,82 @@ static void autocontrol_shadow_level6(void)
|
||||
move_1_forward();
|
||||
}
|
||||
|
||||
/* autocontrol_shadow_level12 (seg002:1184) — единственная тень, с которой
|
||||
* можно и драться, и слиться. Четыре режима подряд, порядок важен.
|
||||
*
|
||||
* 1. «Ждать Кида» (Char.room 15, pop_shadow_init == 0). Пока Кид не дошёл
|
||||
* до x >= 150, тень КАЖДЫЙ КАДР роняется заново — то есть висит в первом
|
||||
* кадре падения у потолка. Дошёл — флаг поднят, падение пущено. Смысл:
|
||||
* Кид обязан УВИДЕТЬ, как она сваливается сверху, а не застать готовую.
|
||||
* 2. У тени вынут меч → обычный боевой ИИ стража. Исключение: если Кид
|
||||
* намеренно убрал клинок (offguard) и тень ещё не отдышалась после
|
||||
* попадания (guard_refrac) — она тоже убирает меч (move_4_down).
|
||||
* 3. У тени меч в ножнах, но Кид ПРИ мече (или не убирал его намеренно):
|
||||
* близко и видно (< 90) — выхватить клинок из стойки (кадр 15); далеко
|
||||
* или не видно — стоять, а если Кид зашёл за спину, попятиться.
|
||||
* 4. Оба без мечей — СЛИЯНИЕ: сойтись ближе 10 и объединиться.
|
||||
*
|
||||
* xdiff инициализируется заведомо большим числом (как в SDLPoP): при
|
||||
* can_guard_see_kid < 2 расстояние не считается вовсе, и ветка «попятиться»
|
||||
* не должна срабатывать на мусоре. */
|
||||
#define FRAME_15_STAND 15
|
||||
#define FRAME_3_START_RUN 3
|
||||
#define FRAME_127_STEPPING_7 127
|
||||
#define FRAME_133_AFTER_STEP 133
|
||||
#define SHADOW_WAIT_KID_X 150
|
||||
#define SHADOW_SWORD_DIST 90
|
||||
|
||||
static void autocontrol_shadow_level12(void)
|
||||
{
|
||||
int16_t distance;
|
||||
|
||||
if (Char.room == SHADOW_FIGHT_ROOM && pop_shadow_init == 0) {
|
||||
if (Opp.x >= SHADOW_WAIT_KID_X) {
|
||||
do_init_shad(init_shad_12, Char.room, SEQ_7_FALL);
|
||||
return;
|
||||
}
|
||||
pop_shadow_init = 1;
|
||||
}
|
||||
if (Char.sword >= SWORD_2_DRAWN) {
|
||||
if (offguard == 0 || guard_refrac == 0) autocontrol_guard_active();
|
||||
else move_4_down();
|
||||
return;
|
||||
}
|
||||
if (Opp.sword >= SWORD_2_DRAWN || offguard == 0) {
|
||||
distance = 0x7000;
|
||||
if (can_guard_see_kid < 2 ||
|
||||
(distance = pop_char_opp_dist()) >= SHADOW_SWORD_DIST) {
|
||||
if (distance < 0) move_2_backward();
|
||||
return;
|
||||
}
|
||||
if (Char.frame == FRAME_15_STAND) move_down_forw();
|
||||
return;
|
||||
}
|
||||
if (pop_char_opp_dist() < SHADOW_UNITE_DIST) {
|
||||
/* Слияние. Кид ПЕРЕЕЗЖАЕТ на место тени: оригинал переписывает
|
||||
* charid активного персонажа (а это тень) в Кида и savekid'ит его
|
||||
* целиком — с позицией, кадром и последовательностью тени. Отсюда
|
||||
* и «дёрганый» шаг в оригинале: Кид оказывается там, где стояла
|
||||
* тень. Слот соперника после этого гасится clear_char. */
|
||||
pop_flash_color = POP_FLASH_WHITE;
|
||||
pop_flash_time = 18;
|
||||
pop_add_life(); /* +1 к потолку HP */
|
||||
pop_united_shadow = SHADOW_UNITE_TIME;
|
||||
Char.charid = CHARID_0_KID;
|
||||
pop_savekid();
|
||||
clear_char();
|
||||
return;
|
||||
}
|
||||
if (can_guard_see_kid == 2) {
|
||||
/* Кид бежит к тени — тень бежит навстречу (кадры бега 3..14 и
|
||||
* шага 127..132). */
|
||||
uint8_t of = Opp.frame;
|
||||
if ((of >= FRAME_3_START_RUN && of < FRAME_15_STAND) ||
|
||||
(of >= FRAME_127_STEPPING_7 && of < FRAME_133_AFTER_STEP))
|
||||
move_1_forward();
|
||||
}
|
||||
}
|
||||
|
||||
/* autocontrol_mouse (seg002:07EB). Весь «ИИ» мыши — два порога по X:
|
||||
* добежала влево до POP_MOUSE_TURN_X → seq_107 (встать, поднять голову,
|
||||
* развернуться и уйти), вернулась вправо до POP_MOUSE_LEAVE_X → исчезнуть.
|
||||
@@ -706,13 +913,13 @@ static void autocontrol_mouse(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* autocontrol_shadow (seg002:081D) — по уровням. Ветка 12 появится со своим
|
||||
* уровнем. */
|
||||
/* autocontrol_shadow (seg002:081D) — по уровням. */
|
||||
static void autocontrol_shadow(void)
|
||||
{
|
||||
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
|
||||
if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5();
|
||||
if (pop_current_level == SHADOW_STEP_LEVEL) autocontrol_shadow_level6();
|
||||
if (pop_current_level == SHADOW_FIGHT_LEVEL) autocontrol_shadow_level12();
|
||||
}
|
||||
|
||||
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
|
||||
@@ -857,8 +1064,7 @@ static void play_guard(void)
|
||||
if (Char.charid == CHARID_24_MOUSE) { autocontrol_opponent(); return; }
|
||||
if (Char.alive < 0 && guardhp_curr == 0) {
|
||||
Char.alive = 0;
|
||||
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
|
||||
* которого нет; для уровня 1 других эффектов там нет. */
|
||||
on_guard_killed(); /* уровень 13: вспышка + «выход открыт» */
|
||||
}
|
||||
autocontrol_opponent();
|
||||
pop_control();
|
||||
|
||||
@@ -464,6 +464,29 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy)
|
||||
pop_env_b(DOOR_FRAM_SLICE[gate_frame], x, yb);
|
||||
}
|
||||
|
||||
/* Та же решётка, но в ПЕРЕДНИЙ слой — поверх персонажа, стоящего в проёме
|
||||
* (draw_gate_fore, seg008:1153). От back-варианта отличается ровно тем, что
|
||||
* есть в оригинале: только ПРОЗРАЧНЫЕ куски (51 низ + 52 восьмипиксельные
|
||||
* ломти), без непрозрачного 50 и без хвостового DOOR_FRAM_SLICE — фон под
|
||||
* баррами уже нарисован, здесь возвращаются только сами прутья.
|
||||
* Клип по прямоугольнику персонажа делает pop_t_fclip (см. pop_tile.h). */
|
||||
static void draw_gate_fore(uint8_t modl, int xh, int dby, int dmy)
|
||||
{
|
||||
int x = xh * 8;
|
||||
int gate_top_y = dby - 62;
|
||||
int openness = (((modl > 188) ? 188 : modl) >> 2) + 1;
|
||||
int gate_bot_y = dmy - openness;
|
||||
int yb;
|
||||
|
||||
pop_env_b(51, x, gate_bot_y - 2);
|
||||
yb = gate_bot_y - 12;
|
||||
if (yb < 192)
|
||||
while (yb >= 0 && yb > 7 && (yb - 7) > gate_top_y) {
|
||||
pop_env_b(52, x, yb);
|
||||
yb -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -599,7 +622,17 @@ static void ceil_over_kid_tile(int col)
|
||||
if (code == 20) { /* draw_tile_bottom: стена */
|
||||
if (!pop_palace) /* seg008:576 */
|
||||
pop_wall_b(POP_WALL_FRAM_BOTTOM[pop_wall_modifier(-1, col)], x, dby);
|
||||
wall_pattern(-1, col, 0);
|
||||
/* wall_pattern ЗДЕСЬ НЕТ — и это не упущение. draw_tile_bottom
|
||||
* (seg008:588) кладёт в foretable ТОЛЬКО кадр грани `id`, под своим
|
||||
* `arg_0`; узор вызывается строкой ниже, вне этой ветки, и уходит в
|
||||
* текущую таблицу, то есть в ФОН. Мы же звали его прямо в переднем
|
||||
* проходе — узор задней стены штамповался поверх всего, что уже
|
||||
* нарисовано, включая падающие плиты (найдено прогоном 2026-08-13).
|
||||
* В фон он попадает там, где и положено, — при отрисовке тайла.
|
||||
*
|
||||
* Не путать с fore_only_tile: там узор в переднем слое ЗАКОННЫЙ,
|
||||
* это порт draw_tile_fore (seg008:715), где он стоит явно рядом с
|
||||
* главной гранью стены, обязанной перекрывать персонажа. */
|
||||
} else if (t->bottom_id) {
|
||||
pop_env_b(t->bottom_id, x, dby);
|
||||
}
|
||||
@@ -948,9 +981,27 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
|
||||
/* на 141 — до него Kid ещё */
|
||||
/* в ряду 1 ПОД полом [0,3]) */
|
||||
if (do_ov) {
|
||||
/* ВЕСЬ диапазон рядов, а не только два крайних. В оригинале
|
||||
* (redraw_at_char2, seg003:0645) стоит ровно два вызова —
|
||||
* char_bottom_row и char_top_row, — и этого хватает, потому что
|
||||
* там они ВСЕГДА соседние. У нас между ними бывает пропуск:
|
||||
* char_footprint форсит rT = rB−1 (голова торчит в ряд выше), а
|
||||
* потом окно fore-клипа расширяет rB ещё на ряд вниз — rT=0,
|
||||
* rB=2 давали обход рядов 0 и 2 МИМО ряда 1.
|
||||
*
|
||||
* Ровно так терялась передняя грань пола у Кида, спрыгнувшего с
|
||||
* кромки: кадр 103, action 3, ряд по y = 1, а обход шёл по 0 и 2
|
||||
* (трасса MAME 2026-08-13: rT=0 rB=2 cL=0 cR=1, ни одного вызова
|
||||
* overlay_mid_tile). Глубже в падении y_to_row давал уже 2, ряд
|
||||
* 1 попадал в пару крайних — и плита вдруг начинала рисоваться
|
||||
* правильно, что и было видно глазом (JUMP-FLOOR-LEGS). */
|
||||
for (c = cR; c >= cL; c--) {
|
||||
if (action != 2) other_overlay_tile(rB, c); /* опорный ряд */
|
||||
if (rT != rB) other_overlay_tile(rT, c); /* верхний ряд */
|
||||
int8_t r2;
|
||||
for (r2 = rB; r2 >= rT; r2--) {
|
||||
/* seg003:0645: при hang_climb опорный ряд НЕ трогаем. */
|
||||
if (r2 == rB && action == 2) continue;
|
||||
other_overlay_tile(r2, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -961,6 +1012,23 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
|
||||
for (r = rT; r <= rB; r++)
|
||||
for (c = cL; c <= cR; c++)
|
||||
fore_tile(r, c);
|
||||
/* Первое условие draw_tile_fore (seg008:0D15): персонаж СТОИТ на тайле
|
||||
* ворот — их решётка возвращается поверх него. Решение приняла карта
|
||||
* (pop_gate_over_char, разбор там же); здесь остаётся отрисовка. Бары
|
||||
* рисуются в колонке curr_col+1: графика ворот принадлежит ПРАВОМУ краю
|
||||
* своего тайла и в оригинале кладётся в проходе следующей колонки.
|
||||
*
|
||||
* Один вызов через трамплин на персонажа за кадр — не в тайловом цикле:
|
||||
* тайл-кандидат всегда ровно один, а fore-проход и так самый горячий
|
||||
* кусок кадра (memory pop_fore_layer_cost). */
|
||||
if (pop_gate_over_char(ch)) {
|
||||
int8_t gr = ch->curr_row, gc = (int8_t)(ch->curr_col + 1);
|
||||
if (tile_in_fclip(gr, gc)) {
|
||||
int dby = 63 * gr + 65;
|
||||
draw_gate_fore(pop_t_bg ? pop_t_bg[gr * 10 + ch->curr_col] : 0,
|
||||
POP_COL_XH[gc], dby, dby - 3);
|
||||
}
|
||||
}
|
||||
/* redraw_needed_above (seg008:0221): полоса кладки У ПОТОЛКА (ряд −1 =
|
||||
* ряд 2 комнаты сверху). В оригинале она рисуется в самом конце обхода
|
||||
* тайлов — ПОСЛЕ объектов, т.е. поверх персонажа (redraw_frames_above,
|
||||
|
||||
@@ -160,6 +160,11 @@ void pop_gate_redraw(int row, int col) __banked;
|
||||
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
|
||||
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
|
||||
void pop_loose_mob_spawn(int row, int col) __banked;
|
||||
/* То же, но в ЯВНО заданной комнате: плита, расшатанная Кидом и брошенная
|
||||
* (он ушёл в соседнюю комнату), доваливается сама — см. animate_loose в
|
||||
* pop_trob.c. Дальше кусок летит обычным путём: mob_tick_one умеет чужую
|
||||
* комнату (mob_tile_at читает уровень, посадку сигналит pop_loose_exit). */
|
||||
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked;
|
||||
void pop_loose_mob_tick(void) __banked;
|
||||
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
|
||||
|
||||
@@ -169,6 +174,9 @@ void pop_loose_mob_room_changed(void) __banked;
|
||||
|
||||
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
|
||||
* seg008:1561). Звать после отрисовки персонажей, перед fore-слоем. */
|
||||
/* Отрисовка падающих кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed()
|
||||
* — фон кадра обязан быть готов до любого спрайта. */
|
||||
void pop_loose_mob_draw(void) __banked;
|
||||
void pop_loose_mob_draw_over(void) __banked;
|
||||
|
||||
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
|
||||
|
||||
@@ -83,11 +83,17 @@ int pop_guard_load(void) __banked
|
||||
"SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl",
|
||||
"SKEL\\g3.atl", 0
|
||||
};
|
||||
/* Джафар (уровень 13, tbl_guard_type == 3) — VIZIER.DAT: кадров столько
|
||||
* же, сколько у стража, страниц пять. */
|
||||
static const char *const vizier_names[GUARD_PAGES] = {
|
||||
"VIZIER\\g0.atl", "VIZIER\\g1.atl", "VIZIER\\g2.atl",
|
||||
"VIZIER\\g3.atl", "VIZIER\\g4.atl"
|
||||
};
|
||||
const char *const *names;
|
||||
int8_t type = pop_level_guard_type(pop_current_level);
|
||||
uint8_t i;
|
||||
if (type < 0) return 0; /* соперников на уровне нет */
|
||||
names = (type == 2) ? skel_names : guard_names;
|
||||
names = (type == 2) ? skel_names : (type == 3) ? vizier_names : guard_names;
|
||||
for (i = 0; i < GUARD_PAGES; i++) {
|
||||
if (names[i] == 0) break;
|
||||
if (atlas_load(&gp[i], names[i]) != 0) return -1;
|
||||
@@ -96,9 +102,10 @@ int pop_guard_load(void) __banked
|
||||
/* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется
|
||||
* из res10.bin по guards_color — это делает pop_guard_set_palette при
|
||||
* входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */
|
||||
if (type == 2) {
|
||||
if (type == 2 || type == 3) {
|
||||
const uint8_t *src = (type == 2) ? pop_skel_pal : pop_vizier_pal;
|
||||
uint8_t buf[64], k;
|
||||
for (k = 0; k < 64; k++) buf[k] = pop_skel_pal[k]; /* BIOS: буфер в W2 */
|
||||
for (k = 0; k < 64; k++) buf[k] = src[k]; /* BIOS: буфер в W2 */
|
||||
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
|
||||
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ int8_t is_guard_notice;
|
||||
uint8_t guard_skill;
|
||||
int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||
int8_t offguard, holding_sword;
|
||||
int8_t pop_united_shadow; /* см. pop_guard.h: 0 / счётчик / −1 */
|
||||
uint8_t pop_shadow_init;
|
||||
int8_t pop_guard_notice_timer; /* фора Джафара (уровень 13) */
|
||||
|
||||
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
|
||||
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
|
||||
@@ -62,11 +65,20 @@ void pop_guard_kill(void)
|
||||
{
|
||||
if (Guard.charid == CHARID_4_SKELETON) return;
|
||||
if (!pop_guard_present()) return;
|
||||
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
|
||||
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
|
||||
* общий control() ставит ему seq_71_dying. */
|
||||
/* HP не обнуляем — ставим дельту, её применит pop_do_delta_hp. Дальше
|
||||
* play_guard увидит «жив, но HP ноль», сам переведёт в alive = 0 и
|
||||
* вызовет on_guard_killed: брызги, вспышка, а на 13-м уровне — флаг
|
||||
* «выход разрешён».
|
||||
*
|
||||
* ОСОЗНАННОЕ РАСХОЖДЕНИЕ (решение пользователя 2026-08-13, ради скорости
|
||||
* прогонов): оригинал (seg000:786) ставит здесь ещё и `Guard.alive = 0`,
|
||||
* а гейт в play_guard (seg006:1490) требует `alive < 0` — то есть у него
|
||||
* чит убивает стража В ОБХОД события смерти, и победа над Джафаром по
|
||||
* читу выхода на 14-й уровень НЕ открывает. Для отладки это дорого:
|
||||
* каждый прогон приходится честно выигрывать бой. Убрав одну строку,
|
||||
* получаем ровно «как будто убил Кид». На игру не влияет — читы в
|
||||
* релизной сборке выключены (pop_cheats). */
|
||||
guardhp_delta = (int8_t)-guardhp_curr;
|
||||
Guard.alive = 0;
|
||||
}
|
||||
|
||||
/* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */
|
||||
@@ -83,11 +95,34 @@ uint8_t pop_take_hp(uint8_t count)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* timers (seg003:0735) — кадровые счётчики спецсобытий.
|
||||
*
|
||||
* Вспышка слияния с тенью досчитывает до нуля и УХОДИТ В −1: это «уже
|
||||
* слились», состояние до конца уровня. Именно на −1 смотрят check_shadow
|
||||
* (больше не поднимать) и «появление плит» в комнатах 2/13 (pop_map).
|
||||
* Ноль как конечное значение не годится — он неотличим от «не было». */
|
||||
void pop_frame_timers(void)
|
||||
{
|
||||
if (pop_united_shadow > 0) {
|
||||
--pop_united_shadow;
|
||||
if (pop_united_shadow == 0) pop_united_shadow = -1;
|
||||
}
|
||||
if (pop_guard_notice_timer > 0) --pop_guard_notice_timer;
|
||||
}
|
||||
|
||||
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
|
||||
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
|
||||
void pop_do_delta_hp(void)
|
||||
{
|
||||
int16_t hp;
|
||||
/* Спецсобытие уровня 12 (seg000:1518): ранил ТЕНЬ — ранил себя. Урон
|
||||
* не делится, а КОПИРУЕТСЯ: тень теряет 1 HP и Кид теряет 1 HP. Без
|
||||
* этого бой с тенью теряет смысл — её можно было бы просто зарубить.
|
||||
* Проверка идёт по слоту соперника (у оригинала — по Opp, но в момент
|
||||
* do_delta_hp там всегда пара Кид/соперник). */
|
||||
if (pop_current_level == SHADOW_FIGHT_LEVEL &&
|
||||
Guard.charid == CHARID_1_SHADOW && guardhp_delta != 0)
|
||||
hitp_delta = guardhp_delta;
|
||||
if (hitp_delta) {
|
||||
hp = (int16_t)hitp_curr + hitp_delta;
|
||||
if (hp < 0) hp = 0;
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
#define CHARID_0_KID 0
|
||||
#define CHARID_1_SHADOW 1
|
||||
#define CHARID_2_GUARD 2
|
||||
/* charid_3 не используется: Джафар (уровень 13) — обычный charid_2_guard,
|
||||
* своим charid оригинал помечает ТОЛЬКО скелета (enter_guard, seg002:196).
|
||||
* Отличают Джафара набор спрайтов (VIZIER.DAT), skill 9 и три события. */
|
||||
#define CHARID_4_SKELETON 4
|
||||
#define CHARID_5_PRINCESS 5
|
||||
/* МЫШЬ (спецсобытие уровня 8) — тоже персонаж слота Guard: своей таблицы
|
||||
@@ -129,8 +132,41 @@
|
||||
|
||||
/* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех
|
||||
* остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см.
|
||||
* pop_check_can_guard_see_kid. */
|
||||
* pop_check_can_guard_see_kid.
|
||||
*
|
||||
* Сцена целиком (seg002:0070 + seg002:1184): в комнате 15 лежит МЕЧ (0,1);
|
||||
* пока он на месте, тень не появляется. Кид его берёт — тень падает в ту
|
||||
* же комнату, и дальше либо бой (у обоих клинки), либо, если Кид уберёт
|
||||
* меч, — слияние. Комната 18 при уходе ВПРАВО «съедает» меч с карты
|
||||
* (sword_disappears), чтобы вернуться и подобрать его второй раз было
|
||||
* нельзя. Проверено по res2012.bin, ../docs/levels_12_15_plan.md §7.3. */
|
||||
#define SHADOW_FIGHT_LEVEL 12
|
||||
#define SHADOW_FIGHT_ROOM 15
|
||||
#define SHADOW_SWORD_TILEPOS 1 /* (кол 1, ряд 0) — меч уровня 12 */
|
||||
#define SWORD_GONE_ROOM 18 /* уход ВПРАВО отсюда убирает меч */
|
||||
/* Слияние (autocontrol_shadow_level12): дистанция, вспышка, её длительность. */
|
||||
#define SHADOW_UNITE_DIST 10
|
||||
#define SHADOW_UNITE_TIME 42
|
||||
|
||||
/* Уровень 13 — ДЖАФАР (custom->jaffar_*, дефолты SDLPoP). Своего ИИ у него
|
||||
* нет вовсе: `autocontrol_Jaffar` (seg002:085A) — это `autocontrol_guard`,
|
||||
* то есть обычный бой стражи. Отличают его только данные (VIZIER.DAT,
|
||||
* skill 9, 6 HP — всё уже в pop_level_cold.c) и три события ниже.
|
||||
*
|
||||
* Встреча: Кид уходит ВПРАВО из комнаты 3 — а `комната 3.right = 1`, где
|
||||
* Джафар и стоит (проверено по res2013.bin, ../docs/levels_12_15_plan.md
|
||||
* §7.2). Ему даётся фора JAFFAR_NOTICE_TIME кадров: пока таймер идёт, он не
|
||||
* встаёт в боевую стойку, и это единственная пауза во всей сцене.
|
||||
*
|
||||
* Выход: смерть Джафара сама дверь НЕ открывает — она ставит
|
||||
* pop_leveldoor_open = 2, а дверь открывает КНОПКА комнаты 24 (tilepos 0,
|
||||
* реальный opener), «нажимаемая» при уходе Кида ВЛЕВО. */
|
||||
#define JAFFAR_LEVEL 13
|
||||
#define JAFFAR_MEET_ROOM 3 /* уход ВПРАВО отсюда будит Джафара */
|
||||
#define JAFFAR_NOTICE_TIME 28 /* ≈2,3 с форы (28/12) */
|
||||
#define JAFFAR_EXIT_ROOM 24
|
||||
#define JAFFAR_EXIT_TILEPOS 0
|
||||
#define JAFFAR_FLASH_TIME 18
|
||||
|
||||
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
|
||||
#define NUM_GUARD_SKILLS 12
|
||||
@@ -184,6 +220,23 @@ extern int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||
* holding_sword — клинок в руке (для индикатора HP стража). */
|
||||
extern int8_t offguard, holding_sword;
|
||||
|
||||
/* Спецсобытие уровня 12 — СЛИЯНИЕ С ТЕНЬЮ (united_with_shadow, seg000:288).
|
||||
* Значение читается как три разных состояния, и это НЕ флаг:
|
||||
* 0 — тень ещё отдельный персонаж (или уровень не 12);
|
||||
* >0 — идёт вспышка слияния, счётчик кадров (42 → 1), убывает в
|
||||
* pop_shadow_timer; пока он идёт, Кид мигает спрайтами тени;
|
||||
* −1 — «уже слились», навсегда до конца уровня. Именно по нему
|
||||
* check_shadow больше не поднимает тень, а в комнатах 2/13
|
||||
* появляются недостающие плиты пола.
|
||||
* Переход 0 → −1 идёт ЧЕРЕЗ счётчик: таймер, досчитав до нуля, уводит его
|
||||
* ещё на единицу вниз (seg003:503) — отсюда и знаковый тип.
|
||||
*
|
||||
* pop_shadow_init (shadow_initialized, seg000:309) — «тень уже дождалась
|
||||
* Кида в комнате 15»: до этого autocontrol_shadow_level12 роняет её заново
|
||||
* каждый кадр, чтобы Кид застал падение, а не готовую стойку. */
|
||||
extern int8_t pop_united_shadow;
|
||||
extern uint8_t pop_shadow_init;
|
||||
|
||||
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
|
||||
extern pop_rnd_t pop_fight_seed;
|
||||
|
||||
@@ -215,6 +268,33 @@ void pop_check_mouse(void) __banked;
|
||||
* возвращается и во втором случае тоже. */
|
||||
uint8_t pop_check_shadow(uint8_t room) __banked;
|
||||
|
||||
/* sword_disappears (seg002:0536) — спецсобытие уровня 12: уход ВПРАВО из
|
||||
* комнаты 18 убирает меч из комнаты 15. Звать при уходе вбок ДО смены
|
||||
* комнаты, рядом с чекпойнтом уровня 3. */
|
||||
void pop_sword_disappears(void) __banked;
|
||||
|
||||
/* meet_Jaffar (seg002:0544) — уход ВПРАВО из комнаты 3 уровня 13 даёт
|
||||
* Джафару фору; звать рядом с pop_sword_disappears (та же ветка leave_room).
|
||||
* Jaffar_exit (seg002:0517) — уход ВЛЕВО при pop_leveldoor_open == 2 жмёт
|
||||
* кнопку комнаты 24 и открывает дверь уровня. */
|
||||
void pop_meet_jaffar(void) __banked;
|
||||
void pop_jaffar_exit(void) __banked;
|
||||
|
||||
/* check_killed_shadow (seg006:2033) — спецсобытие уровня 12: смерть тени
|
||||
* убивает Кида и наоборот. Звать раз в кадр ДО тиков персонажей, рядом с
|
||||
* pop_check_skel; на прочих уровнях выходит первой строкой. */
|
||||
void pop_check_killed_shadow(void) __banked;
|
||||
|
||||
/* timers (seg003:0735) — кадровые счётчики спецсобытий: вспышка слияния с
|
||||
* тенью и фора Джафара. Звать раз в кадр из главного цикла. Резидент: два
|
||||
* вычитания, банк ради них не разворачиваем. */
|
||||
void pop_frame_timers(void);
|
||||
|
||||
/* guard_notice_timer (seg000:307) — фора Джафара после встречи: пока >0, он
|
||||
* не встаёт в боевую стойку. Ставит pop_meet_jaffar, убавляет
|
||||
* pop_frame_timers, читает autocontrol_guard_inactive. */
|
||||
extern int8_t pop_guard_notice_timer;
|
||||
|
||||
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
|
||||
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
|
||||
void pop_check_sword_hurting(void) __banked;
|
||||
|
||||
@@ -102,8 +102,15 @@ void pop_guard_enter(uint8_t room)
|
||||
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
|
||||
/* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты
|
||||
* палитры общие на экран, и смена посреди кадра дала бы стража в новой
|
||||
* палитре с полосой HP в старой. */
|
||||
pop_guard_set_palette(color);
|
||||
* палитре с полосой HP в старой.
|
||||
*
|
||||
* Цвет из данных комнаты берётся ТОЛЬКО у обычного стража (enter_guard,
|
||||
* seg002:183: `curr_guard_color = 0`, если tbl_guard_type != 0). У
|
||||
* скелета и Джафара палитра своя, из файла набора, и подменять её
|
||||
* нельзя — иначе визирь на уровне 13 покрасился бы в цвет стража,
|
||||
* записанный в его комнате. */
|
||||
pop_guard_set_palette(pop_level_guard_type(pop_current_level) == 0
|
||||
? color : 0);
|
||||
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
|
||||
* колонки тайла, а не берётся из файла. */
|
||||
Guard.room = room;
|
||||
|
||||
@@ -28,4 +28,9 @@ static const uint8_t pop_skel_pal[64] = {
|
||||
0x00,0x00,0x00,0x00,0xFF,0xFF,0x55,0x00,0x89,0x79,0xBA,0x00,0xF2,0xFF,0xFF,0x00,0x00,0x00,0xAA,0x00,0xAA,0x00,0xAA,0x00,0x00,0x55,0xAA,0x00,0xDA,0xE6,0xE6,0x00,0x55,0x55,0x55,0x00,0xFF,0x55,0x55,0x00,0x55,0xFF,0x55,0x00,0xFF,0xFF,0x55,0x00,0x55,0x55,0xFF,0x00,0x00,0x00,0x00,0x00,0x55,0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0x00,
|
||||
};
|
||||
|
||||
/* Джафар (VIZIER/res750.pal): свой набор — свой цвет. */
|
||||
static const uint8_t pop_vizier_pal[64] = {
|
||||
0x00,0x00,0x00,0x00,0x79,0x69,0xFF,0x00,0x7D,0x4C,0xBE,0x00,0xFF,0xFF,0xFF,0x00,0x38,0x5D,0x79,0x00,0x50,0x6D,0xAA,0x00,0x69,0x89,0xDA,0x00,0xD2,0xB6,0xD2,0x00,0x91,0x7D,0x91,0x00,0xFF,0xDA,0xFF,0x00,0x4C,0x81,0xA1,0x00,0x71,0xBA,0xEA,0x00,0xEA,0x44,0xB2,0x00,0xEA,0x44,0xB2,0x00,0x55,0xFF,0xFF,0x00,0x99,0x2C,0x75,0x00,
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -236,6 +236,16 @@ void pop_kid_hp_reset(void) __banked
|
||||
pop_kid_dead = 0;
|
||||
}
|
||||
|
||||
/* Бесшовный переход (seamless, seg003:158): уровень стартует, НЕ трогая HP —
|
||||
* Кид уносит на следующий и потолок, и полученный урон, потому что для него
|
||||
* это не «новый уровень», а продолжение того же бега. Всё остальное, что
|
||||
* делает pop_kid_hp_reset, нужно и здесь: дельта кадра и признак смерти. */
|
||||
void pop_kid_hp_carry(void) __banked
|
||||
{
|
||||
hitp_delta = 0;
|
||||
pop_kid_dead = 0;
|
||||
}
|
||||
|
||||
/* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */
|
||||
void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; }
|
||||
|
||||
@@ -249,25 +259,39 @@ static void pop_kid_add_life(void)
|
||||
hitp_delta = (int8_t)(hitp_max - hitp_curr);
|
||||
}
|
||||
|
||||
/* Тот же add_life наружу: слияние с тенью (уровень 12) даёт ровно ту же
|
||||
* прибавку, что большая склянка, а живёт ИИ тени в другом банке. */
|
||||
void pop_add_life(void) __banked { pop_kid_add_life(); }
|
||||
|
||||
/* Урон — через общий pop_take_hp (резидент pop_guard.c): физика работает
|
||||
* над Char, значит и HP обязаны убавляться у ТОГО, кто сейчас в Char. Своя
|
||||
* копия take_hp здесь была Kid-only и правила hitp_curr мимо дельты. */
|
||||
|
||||
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked
|
||||
/* Перечитать fg соседей из уровня, НЕ трогая историю коллизий. Отдельно от
|
||||
* pop_map_set_edges, потому что нужен и посреди жизни в комнате: тайл соседа
|
||||
* может измениться, пока тот не отрисован (плита, брошенная Кидом на
|
||||
* полпути, доваливается заочно — animate_loose в pop_trob.c). g_lcol/g_rcol
|
||||
* до этого были снимком НА МОМЕНТ ВХОДА, и коллизия у шва продолжала считать
|
||||
* провалившуюся плиту полом. */
|
||||
void pop_map_refresh_edges(void) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
if (!g_link_l && !g_link_r) return;
|
||||
pop_level_access_begin();
|
||||
for (i = 0; i < 30; i++) {
|
||||
if (g_link_l) g_lcol[i] = pop_level_tile_raw(g_link_l, i);
|
||||
if (g_link_r) g_rcol[i] = pop_level_tile_raw(g_link_r, i);
|
||||
}
|
||||
pop_level_access_end();
|
||||
}
|
||||
|
||||
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked
|
||||
{
|
||||
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
|
||||
/* fg соседей — прямо из уровня, одной скобкой доступа на обе комнаты.
|
||||
* Держать ещё одну копию в приложении незачем: карта — единственный её
|
||||
* потребитель (отрисовке швов хватает кромочных колонок pop_room_load). */
|
||||
if (left || right) {
|
||||
pop_level_access_begin();
|
||||
for (i = 0; i < 30; i++) {
|
||||
if (left) g_lcol[i] = pop_level_tile_raw(left, i);
|
||||
if (right) g_rcol[i] = pop_level_tile_raw(right, i);
|
||||
}
|
||||
pop_level_access_end();
|
||||
}
|
||||
pop_map_refresh_edges();
|
||||
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
|
||||
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и
|
||||
* бампа на кадре входа нет). БОКОВОЙ переход это отменяет: вызывающий
|
||||
@@ -1349,13 +1373,52 @@ uint8_t pop_get_item_action(void) __banked
|
||||
}
|
||||
}
|
||||
|
||||
/* Спецсобытие уровня 12 «появление плит» (seg006:1063, ветка внутри
|
||||
* check_on_floor). После слияния с тенью (pop_united_shadow == −1) в
|
||||
* верхнем ряду комнат 2 и 13 пропасти становятся проходимыми: персонаж,
|
||||
* который ДОЛЖЕН БЫЛ провалиться, вместо этого достраивает под собой пол.
|
||||
* Плита появляется ровно под ним, шаг за шагом — это не «включили этаж», а
|
||||
* дорожка, возникающая под ногами.
|
||||
*
|
||||
* Возврат 1 = «пол появился, падать не надо». Перерисовываем сам тайл и
|
||||
* соседа справа на ОБЕИХ страницах — тем же приёмом, что pop_skel_wake_tile
|
||||
* (у оригинала это пара set_redraw_full/set_wipe на curr_tilepos и +1).
|
||||
*
|
||||
* Комната берётся РЕЗОЛВНУТОЙ (tile_room_of): персонаж может стоять в шве,
|
||||
* и тогда пол обязан появиться в соседней комнате, как и у оригинала —
|
||||
* там get_tile переставляет curr_room/curr_tilepos до присваивания. */
|
||||
#define HIDDEN_FLOOR_ROOM_A 2
|
||||
#define HIDDEN_FLOOR_ROOM_B 13
|
||||
#define HIDDEN_FLOOR_COL_B 6 /* в комнате 13 — только правая половина */
|
||||
|
||||
static uint8_t floors_appear(void)
|
||||
{
|
||||
uint8_t room, tp;
|
||||
if (pop_current_level != SHADOW_FIGHT_LEVEL) return 0;
|
||||
if (pop_united_shadow >= 0) return 0; /* только ПОСЛЕ слияния */
|
||||
if (Char.curr_row != 0) return 0;
|
||||
if (Char.room != HIDDEN_FLOOR_ROOM_A &&
|
||||
!(Char.room == HIDDEN_FLOOR_ROOM_B && Char.curr_col >= HIDDEN_FLOOR_COL_B))
|
||||
return 0;
|
||||
room = tile_room_of(Char.curr_col, Char.curr_row, &tp);
|
||||
if (!room) return 0;
|
||||
pop_level_set_tile(room, tp, TILE_FLOOR);
|
||||
if (room == g_room) {
|
||||
g_fg[tp] = TILE_FLOOR;
|
||||
pop_set_redraw(tp, POP_RD_FLOOR, 2);
|
||||
if ((tp % 10) < 9)
|
||||
pop_set_redraw((uint8_t)(tp + 1), POP_RD_FLOOR, 2);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- Диспетчер ----------------------------------------------------- */
|
||||
static void check_on_floor(void)
|
||||
{
|
||||
if (kid_cur_flags() & FRAME_NEEDS_FLOOR) {
|
||||
if (get_tile_at_char() == TILE_WALL)
|
||||
in_wall(); /* забежал в стену -> выровнять */
|
||||
if (!tile_is_floor(g_curr_tile))
|
||||
if (!tile_is_floor(g_curr_tile) && !floors_appear())
|
||||
start_fall();
|
||||
}
|
||||
}
|
||||
@@ -1466,6 +1529,40 @@ static void bumped_floor(void)
|
||||
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
|
||||
}
|
||||
|
||||
/* bump_into_opponent (seg003:08AA). Зовётся из play_kid_frame (seg000:1238)
|
||||
* между set_char_collision и check_collisions, и ТОЛЬКО у Кида — у стража
|
||||
* этой ветки нет (см. комментарий к guard_phys ниже).
|
||||
*
|
||||
* ЗАЧЕМ. Это единственная защита оригинала от «вошёл в комнату — сразу
|
||||
* труп». Стража при входе никуда не отбрасывает: enter_guard (seg002:0112)
|
||||
* ставит его ровно в его тайл из данных уровня, и Кид вполне может войти
|
||||
* вплотную. Спасает то, что БЕЗОРУЖНЫЙ Кид, столкнувшись с вооружённым
|
||||
* соперником лицом к лицу, не получает укол, а ОТСКАКИВАЕТ (seq_47).
|
||||
* Остальные три страховки у нас уже есть: Кид сам достаёт меч
|
||||
* (control_standing, pop_ctrl.c), страж поднимается в стойке покоя с мечом
|
||||
* в ножнах (pop_guard_cold.c) и не колет ближе 12 (guards.c).
|
||||
*
|
||||
* Расхождений с оригиналом нет. Опциональные фиксы SDLPoP
|
||||
* (fix_painless_fall_on_guard — падение на стража с высоты перестаёт быть
|
||||
* безболезненным; fix_jumping_over_guard — нельзя перепрыгнуть) намеренно НЕ
|
||||
* портированы: базовое поведение движка 1989 года. */
|
||||
static void bump_into_opponent(void)
|
||||
{
|
||||
int16_t distance;
|
||||
if (can_guard_see_kid < 2) return;
|
||||
if (Char.sword != 0) return; /* Кид уже в боевой стойке */
|
||||
if (Opp.sword == 0) return; /* у соперника меч в ножнах */
|
||||
if (Opp.action >= 2) return; /* только stand / run_jump */
|
||||
if (Char.direction == Opp.direction) return; /* должны быть лицом к лицу */
|
||||
distance = pop_char_opp_dist();
|
||||
if (distance < 0) distance = -distance;
|
||||
if (distance > 15) return;
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
pop_char_set_seq(SEQ_47_BUMP);
|
||||
play_seq();
|
||||
}
|
||||
|
||||
/* ---- check_collisions (seg004:0004) -------------------------------- *
|
||||
* ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка
|
||||
* удара выбирается обходом 9→0 по переходу флага 0→1.
|
||||
@@ -2112,6 +2209,12 @@ static void check_press(void)
|
||||
static void do_knock(int tile_row)
|
||||
{
|
||||
int col, pos;
|
||||
/* СПЕЦКЕЙС УРОВНЯ 13 (loose_make_shake, seg007:949): сотрясение НЕ
|
||||
* трясёт плиты вовсе. Причина та же, что у первого спецкейса: тряска
|
||||
* ставит фазу 0x80, а на этом уровне у плит уже лежит СВОЙ отложенный
|
||||
* старт (0xF0..0xFF), и приземление Кида сбросило бы его на 0x80 —
|
||||
* гряда посыпалась бы разом вместо вразнобой. */
|
||||
if (pop_current_level == POP_LOOSE_TILES_LEVEL) return;
|
||||
if (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */
|
||||
if (!g_above || !g_link_u) return;
|
||||
for (col = 0; col < 10; col++)
|
||||
@@ -2139,18 +2242,120 @@ void pop_check_knock(void) __banked
|
||||
}
|
||||
}
|
||||
|
||||
/* Сдать НЕЗАКОНЧЕННЫЕ loose уходящей комнаты в персистентное хранилище —
|
||||
* звать ПЕРЕД сменой комнаты, пока g_room/g_fg/g_link_u ещё старые.
|
||||
*
|
||||
* pop_loose_modif[30] — фаза тайлов ТЕКУЩЕЙ комнаты, и раньше смена комнаты
|
||||
* её просто теряла: плита, по которой Кид пробежал и ушёл вправо, оставалась
|
||||
* целой (уровень 10, комната 2, тайл 2,9). В оригинале терять нечего —
|
||||
* там loose это trob в общем списке, а фаза лежит в curr_room_modif, то есть
|
||||
* в самой таблице уровня. Порт того же: фаза уезжает в room_modif (он
|
||||
* персистентен per-room), а досчитывает её animate_loose в pop_trob.c.
|
||||
*
|
||||
* Пишем фазу ВСЕМ loose-тайлам, включая нулевые: room_modif — источник
|
||||
* истины, а pop_loose_modif лишь его копия на время пребывания в комнате,
|
||||
* и оставить там прошлое значение значило бы «оживить» уже отдрожавшую
|
||||
* плиту при следующем входе. trob заводим только на незаконченные. */
|
||||
void pop_loose_leave_room(void) __banked
|
||||
{
|
||||
uint8_t *mod;
|
||||
int i;
|
||||
if (!g_fg || !g_room) return;
|
||||
mod = pop_trob_modif(g_room);
|
||||
if (mod) {
|
||||
for (i = 0; i < 30; i++) {
|
||||
if ((g_fg[i] & 0x1F) != TILE_LOOSE) continue;
|
||||
mod[i] = pop_loose_modif[i];
|
||||
if (pop_loose_modif[i]) pop_add_trob(g_room, (uint8_t)i, 0);
|
||||
}
|
||||
}
|
||||
/* Плиты-ПОТОЛКИ — ряд 2 комнаты сверху, tilepos 20+col. */
|
||||
if (g_above && g_link_u) {
|
||||
mod = pop_trob_modif(g_link_u);
|
||||
if (mod) {
|
||||
for (i = 0; i < 10; i++) {
|
||||
if ((g_above[i] & 0x1F) != TILE_LOOSE) continue;
|
||||
mod[20 + i] = pop_ceil_modif[i];
|
||||
if (pop_ceil_modif[i])
|
||||
pop_add_trob(g_link_u, (uint8_t)(20 + i), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Решётка ворот должна идти ПОВЕРХ персонажа? Порт первого условия
|
||||
* draw_tile_fore (seg008:0D15):
|
||||
*
|
||||
* if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
|
||||
* Kid.curr_col == drawn_col - 1 && Kid.room != room_R)
|
||||
* draw_gate_fore();
|
||||
*
|
||||
* То есть: рисуя тайл (row, col), оригинал кладёт бары ворот в FOREtable —
|
||||
* поверх всего, что уже нарисовано, — если тайл СЛЕВА это ворота и персонаж
|
||||
* стоит ИМЕННО НА НИХ. Графика ворот принадлежит правому краю своего тайла
|
||||
* и рисуется в проходе СЛЕДУЮЩЕЙ колонки, отсюда сдвиг на единицу.
|
||||
*
|
||||
* Живёт в карте, а не в слое фона, по двум причинам: спрашивает она про
|
||||
* ТАЙЛ и позицию персонажа (это знание карты), и её линкует хост-харнесс —
|
||||
* решение проверяется тестом без эмулятора (t_char: gate_fore_over_char).
|
||||
* Слою фона остаётся только рисование (draw_gate_fore в pop_bg.c).
|
||||
*
|
||||
* РАСХОЖДЕНИЕ С ОРИГИНАЛОМ (осознанное, см. docs/impl_diff.md): оригинал
|
||||
* жёстко спрашивает про Kid, потому что foretable у него ОДНА на проход
|
||||
* тайлов; у нас fore-проход идёт по персонажу, поэтому спрашиваем про того,
|
||||
* кого рисуем. Наш вариант — надмножество: страж, стоящий в проёме ворот,
|
||||
* у нас тоже уходит ЗА решётку, а в оригинале остался бы перед ней. */
|
||||
uint8_t pop_gate_over_char(const pop_char_t *ch) __banked
|
||||
{
|
||||
if (!ch || ch->room != g_room) return 0;
|
||||
if (ch->curr_row < 0 || ch->curr_row > 2) return 0;
|
||||
/* Колонка 9: бары легли бы в колонку 10, то есть в СОСЕДНЮЮ комнату —
|
||||
* у оригинала это отсекает `Kid.room != room_R`. */
|
||||
if (ch->curr_col < 0 || ch->curr_col > 8) return 0;
|
||||
return (uint8_t)(get_tile(ch->curr_col, ch->curr_row) == TILE_GATE);
|
||||
}
|
||||
|
||||
/* СТАРТ/РЕСТАРТ уровня: забыть фазы, НЕ сдавая их в room_modif — в
|
||||
* оригинале всё состояние сносит сам load_level (seg003:57). Без этого
|
||||
* pop_loose_leave_room у первого же enter_room занесла бы в только что
|
||||
* восстановленный уровень дрожь из прошлой жизни: pop_trob_reset уже
|
||||
* отработал, room_modif свежий, а pop_loose_modif — ещё старый. */
|
||||
void pop_loose_forget(void) __banked
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 30; i++) pop_loose_modif[i] = 0;
|
||||
for (i = 0; i < 10; i++) pop_ceil_modif[i] = 0;
|
||||
}
|
||||
|
||||
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
|
||||
* индексируются позицией тайла и не должны утечь из старой комнаты в новую.
|
||||
* Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и
|
||||
* должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */
|
||||
* должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed).
|
||||
*
|
||||
* Вместо нулей забираем фазу НОВОЙ комнаты из room_modif — обратная
|
||||
* половина pop_loose_leave_room: расшатанная и брошенная плита, к которой
|
||||
* Кид вернулся, продолжает дрожать с той же фазы, а не с покоя. trob,
|
||||
* который крутил её заочно, снимет сам animate_loose (room == cur_room). */
|
||||
void pop_loose_reset(void) __banked
|
||||
{
|
||||
uint8_t *mod;
|
||||
int i;
|
||||
|
||||
mod = g_room ? pop_trob_modif(g_room) : 0;
|
||||
for (i = 0; i < 30; i++) {
|
||||
pop_loose_modif[i] = 0;
|
||||
pop_loose_modif[i] = (mod && g_fg && (g_fg[i] & 0x1F) == TILE_LOOSE)
|
||||
? mod[i] : 0;
|
||||
}
|
||||
mod = (g_above && g_link_u) ? pop_trob_modif(g_link_u) : 0;
|
||||
for (i = 0; i < 10; i++) {
|
||||
pop_ceil_modif[i] = 0;
|
||||
pop_ceil_modif[i] = (mod && (g_above[i] & 0x1F) == TILE_LOOSE)
|
||||
? mod[20 + i] : 0;
|
||||
/* ...и СНЯТЬ заочный trob: плиту-потолок отсюда снова ведёт
|
||||
* pop_loose_tick. Без этого её крутили бы ДВОЕ — trob по
|
||||
* room_modif и мы по pop_ceil_modif, — и она проваливалась бы
|
||||
* дважды (animate_loose снимает сам только trob'ы cur_room, а
|
||||
* комната сверху — не cur_room). */
|
||||
if (pop_ceil_modif[i]) pop_add_trob(g_link_u, (uint8_t)(20 + i), -1);
|
||||
}
|
||||
pop_ceil_fell = 0;
|
||||
pop_debris_at = 0;
|
||||
@@ -2158,6 +2363,82 @@ void pop_loose_reset(void) __banked
|
||||
pop_loose_mob_room_changed();
|
||||
}
|
||||
|
||||
/* remove_loose (seg007:0EB8) для ОДНОГО тайла, наружу — ради честного
|
||||
* loose_fall: падающий кусок сбивает плиту НЕМЕДЛЕННО, в том же кадре
|
||||
* (pop_room.c, mob_break_loose). Здесь, а не в pop_room, потому что тут
|
||||
* живут и рабочая копия тайлов комнаты (g_fg), и фазы плит.
|
||||
*
|
||||
* Перерисовываем ДВЕ клетки — саму и соседа справа. Передний торец пола
|
||||
* заезжает в соседний тайл (правая грань соседа всегда в backtable), и без
|
||||
* второй пометки от исчезнувшей плиты оставался висеть торец: тайл пустой,
|
||||
* а его передняя грань нарисована. Тем же приёмом, что pop_skel_wake_tile
|
||||
* и что set_redraw_full+set_wipe на curr_tilepos и curr_tilepos+1 в
|
||||
* оригинале. */
|
||||
void pop_loose_remove(uint8_t room, uint8_t tilepos) __banked
|
||||
{
|
||||
uint8_t *mod = pop_trob_modif(room);
|
||||
if (tilepos >= 30) return;
|
||||
if (room == g_room) {
|
||||
g_fg[tilepos] = TILE_EMPTY;
|
||||
pop_loose_modif[tilepos] = 0;
|
||||
pop_set_redraw(tilepos, POP_RD_LOOSE_GONE, 2);
|
||||
if ((tilepos % 10) < 9)
|
||||
pop_set_redraw((uint8_t)(tilepos + 1), POP_RD_FLOOR, 2);
|
||||
} else {
|
||||
/* Комната не на экране, но её кромочная колонка видна в шве — те же
|
||||
* снимки, что обновляет animate_loose. */
|
||||
pop_neigh_dirty = 1;
|
||||
}
|
||||
/* Модификатор пустой клетки = ТИП УРОВНЯ (по нему draw_tile_right
|
||||
* выбирает blueline у дыры), тайл в странице уровня — сразу. */
|
||||
if (mod) mod[tilepos] = pop_palace;
|
||||
pop_level_set_tile(room, tilepos, TILE_EMPTY);
|
||||
}
|
||||
|
||||
/* check_fall_flo (seg000:1317) — СПЕЦСОБЫТИЕ уровня 13: гряда плит над
|
||||
* комнатой сыплется на Кида сама, вразнобой.
|
||||
*
|
||||
* Как это устроено в оригинале: при входе в комнату 23 или 16 шести плитам
|
||||
* ряда 2 комнаты СВЕРХУ раздаётся ОТРИЦАТЕЛЬНЫЙ модификатор −(rnd & 15).
|
||||
* Отрицательное значение — это старший бит, то есть для animate_loose оно
|
||||
* выглядит как «тряска», а на уровне 13 тряска намеренно не гасится
|
||||
* (спецкейс в pop_loose_tick). Фаза считает вверх, доходит до нуля и
|
||||
* дальше идёт обычным отсчётом до провала — получается разная задержка у
|
||||
* каждой плиты.
|
||||
*
|
||||
* Плиты ряда 2 верхней комнаты у нас — это pop_ceil_modif (плиты-потолки),
|
||||
* так что раздаём фазу именно туда.
|
||||
*
|
||||
* ОДНО отличие от оригинала, вынужденное представлением: у нас фаза 0 = «не
|
||||
* анимируется», плита с такой фазой в цикле пропускается вовсе. Значит
|
||||
* счёт, дойдя до нуля, оборвался бы — поэтому, во-первых, тик перескакивает
|
||||
* ноль (см. pop_loose_tick), а во-вторых, стартовое значение берётся на
|
||||
* единицу «отрицательнее» (−n−1): вместе это даёт ровно ту же суммарную
|
||||
* задержку n + 11 кадров, что и у оригинала.
|
||||
*
|
||||
* Звать при входе в комнату, после pop_loose_reset (тот забирает фазы из
|
||||
* room_modif) — как check_the_end зовёт его после start_chompers. */
|
||||
static pop_rnd_t loose_seed; /* свой домен последовательности, см. pop_geom.h */
|
||||
|
||||
void pop_check_fall_flo(void) __banked
|
||||
{
|
||||
uint8_t col;
|
||||
if (pop_current_level != POP_LOOSE_TILES_LEVEL) return;
|
||||
if (g_room != POP_LOOSE_TILES_ROOM1 && g_room != POP_LOOSE_TILES_ROOM2) return;
|
||||
if (!g_above || !g_link_u) return;
|
||||
for (col = POP_LOOSE_TILES_FIRST - 20; col <= POP_LOOSE_TILES_LAST - 20; col++) {
|
||||
uint8_t t = g_above[col];
|
||||
/* Те же отсевы, что в make_loose_fall: не loose и «solid» loose
|
||||
* пропускаются молча (в комнате 1 тайл 24 пустой — диапазон 22..27
|
||||
* намеренно надмножество), уже идущий отсчёт не перебиваем. */
|
||||
if ((t & 0x1F) != TILE_LOOSE || (t & 0x20)) continue;
|
||||
if ((int8_t)pop_ceil_modif[col] > 0) continue;
|
||||
pop_ceil_modif[col] =
|
||||
(uint8_t)(-(int8_t)(pop_prandom(&loose_seed, 255) & 0x0F) - 1);
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
|
||||
* (кадры 5..14) не бьёт (кроме уровня 13 — его тут нет); в висе/подъёме тоже
|
||||
* (action >= hang_climb, кроме turn). Иначе −1 HP: смерть → seq_22 (crushed),
|
||||
@@ -2169,7 +2450,10 @@ void pop_loose_reset(void) __banked
|
||||
static void fell_on_your_head(void)
|
||||
{
|
||||
uint8_t frame = Char.frame, action = Char.action;
|
||||
if ((frame < 5 || frame >= 15) &&
|
||||
/* СПЕЦКЕЙС УРОВНЯ 13 (seg007:1218): там плита бьёт и В БЕГЕ. На прочих
|
||||
* уровнях бегущего она пропускает — иначе гряда падающих плит, сквозь
|
||||
* которую и надо пробегать, не представляла бы никакой угрозы. */
|
||||
if ((pop_current_level == POP_LOOSE_TILES_LEVEL || frame < 5 || frame >= 15) &&
|
||||
(action < ACT_HANGCLIMB || action == ACT_TURN)) {
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
if (pop_take_hp(1)) { /* «брызги» даст pop_do_delta_hp */
|
||||
@@ -2221,8 +2505,22 @@ void pop_loose_tick(void) __banked
|
||||
for (pos = 0; pos < 30; pos++) {
|
||||
if (pop_loose_modif[pos] != 0) {
|
||||
uint8_t m = ++pop_loose_modif[pos];
|
||||
/* Отложенный старт уровня 13 досчитал до нуля. Ноль у нас
|
||||
* означает «не анимируется», и плита застряла бы навсегда —
|
||||
* перескакиваем на 1. Стартовое значение выбрано с учётом
|
||||
* этого перескока (см. pop_check_fall_flo). */
|
||||
if (m == 0) m = pop_loose_modif[pos] = 1;
|
||||
if (m & 0x80) { /* тряска (do_knock) */
|
||||
if (m >= 0x84) { /* конец тряски: сброс + покой */
|
||||
/* СПЕЦКЕЙС УРОВНЯ 13 (seg007:823): фазу со старшим битом НЕ
|
||||
* гасим и не трясём. На нём check_fall_flo раздаёт плитам
|
||||
* ОТРИЦАТЕЛЬНЫЙ старт (0xF0..0xFF) как отложенный таймер, и
|
||||
* обычная ветка «тряска кончилась на 0x84» убила бы его в
|
||||
* первом же кадре — плита не упала бы никогда. Пропущенная
|
||||
* инкрементом фаза сама дойдёт до 0x00, там старший бит
|
||||
* снимется, и дальше пойдёт обычный отсчёт до провала. */
|
||||
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
|
||||
/* ждём молча */
|
||||
} else if (m >= 0x84) { /* конец тряски: сброс + покой */
|
||||
pop_loose_modif[pos] = 0;
|
||||
/* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
|
||||
* правая грань loose живёт в соседе). ОБЕ страницы —
|
||||
@@ -2231,6 +2529,13 @@ void pop_loose_tick(void) __banked
|
||||
} else {
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */
|
||||
}
|
||||
} else if (m >= 11 && pop_loose_fell) {
|
||||
/* Сигнал прошлого провала ещё не разобран главным циклом (он
|
||||
* ОДИН на кадр) — придержим плиту кадр, откатив фазу к 10.
|
||||
* Иначе из двух плит, провалившихся в одном кадре, дыру
|
||||
* запекала бы только вторая, а первая оставалась нарисованной
|
||||
* целой (уровень 13: check_fall_flo роняет гряду разом). */
|
||||
pop_loose_modif[pos] = 10;
|
||||
} else if (m >= 11) { /* loose_floor_delay — падение */
|
||||
/* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
|
||||
* Debris появляется ТОЛЬКО там, где приземлился падающий
|
||||
@@ -2244,6 +2549,11 @@ void pop_loose_tick(void) __banked
|
||||
pop_trob_modif(g_room)[pos] = pop_palace;
|
||||
pop_loose_modif[pos] = 0;
|
||||
pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2); /* запечь пусто, обе стр. */
|
||||
/* ...и СОСЕДА СПРАВА: передний торец пола заезжает в его
|
||||
* клетку, и без этой пометки он оставался висеть над пустым
|
||||
* тайлом. То же, что делает pop_skel_wake_tile. */
|
||||
if ((pos % 10) < 9)
|
||||
pop_set_redraw((uint8_t)(pos + 1), POP_RD_FLOOR, 2);
|
||||
pop_loose_mob_spawn(row, col); /* отрыв падающего куска */
|
||||
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
|
||||
} else { /* отсчёт 1..10 — дрожащий кадр */
|
||||
@@ -2258,10 +2568,19 @@ void pop_loose_tick(void) __banked
|
||||
for (col = 0; col < 10; col++) {
|
||||
if (pop_ceil_modif[col] != 0) {
|
||||
uint8_t m = ++pop_ceil_modif[col];
|
||||
if (m == 0) m = pop_ceil_modif[col] = 1; /* см. pop_loose_tick выше */
|
||||
if (m & 0x80) { /* тряска от сотрясения (do_knock) */
|
||||
if (m >= 0x84) { pop_ceil_modif[col] = 0;
|
||||
/* Тот же спецкейс уровня 13, и здесь он ГЛАВНЫЙ: плиты,
|
||||
* которым check_fall_flo раздал отложенный старт, лежат
|
||||
* именно в ряду 2 комнаты СВЕРХУ, то есть ведёт их этот
|
||||
* цикл, а не соседний. */
|
||||
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
|
||||
/* ждём молча */
|
||||
} else if (m >= 0x84) { pop_ceil_modif[col] = 0;
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
|
||||
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
|
||||
} else if (m >= 11 && pop_ceil_fell) {
|
||||
pop_ceil_modif[col] = 10; /* сигнал занят — кадр подождём */
|
||||
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
|
||||
pop_ceil_modif[col] = 0;
|
||||
if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */
|
||||
@@ -2629,6 +2948,7 @@ static void kid_phys(void)
|
||||
fall_accel();
|
||||
fall_speed();
|
||||
determine_col();
|
||||
bump_into_opponent(); /* seg003: безоружный Кид отскакивает от стража */
|
||||
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
|
||||
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
|
||||
check_action();
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#define POP_MAP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */
|
||||
|
||||
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
|
||||
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
||||
@@ -27,6 +28,10 @@ void pop_map_set(uint8_t *fg) __banked;
|
||||
* то есть заведомо выходит за комнату; кромочных колонок не хватало, и
|
||||
* get_tile отдавал мнимую стену (BUG-SWORD-GHOST-1). Звать в enter_room. */
|
||||
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked;
|
||||
/* Перечитать fg соседей (кромочные колонки коллизии) из уровня, не сбрасывая
|
||||
* историю перекрытий. Нужен, когда тайл соседа изменился, пока тот не
|
||||
* отрисован — см. pop_neigh_dirty в pop_state.h. */
|
||||
void pop_map_refresh_edges(void) __banked;
|
||||
|
||||
/* БОКОВОЙ переход в соседнюю комнату: перенумеровать историю флагов
|
||||
* перекрытия вместо того, чтобы её выбросить (иначе Кид с разбега проходит
|
||||
@@ -64,7 +69,15 @@ extern uint8_t pop_kid_dead;
|
||||
extern uint8_t hitp_curr;
|
||||
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
|
||||
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
|
||||
/* check_fall_flo (seg000:1317) — спецсобытие уровня 13: вход в комнату 23
|
||||
* или 16 роняет на Кида гряду плит из комнаты СВЕРХУ, каждую со своей
|
||||
* задержкой. Звать при входе в комнату ПОСЛЕ pop_loose_reset (порядок
|
||||
* check_the_end: start_chompers → check_fall_flo → check_shadow). */
|
||||
void pop_check_fall_flo(void) __banked;
|
||||
|
||||
void pop_kid_hp_reset(void) __banked;
|
||||
/* То же для БЕСШОВНОГО старта уровня (pop_seamless): HP не трогаем вовсе. */
|
||||
void pop_kid_hp_carry(void) __banked;
|
||||
|
||||
/* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт
|
||||
* уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень
|
||||
@@ -89,8 +102,14 @@ extern uint8_t pop_flash_time;
|
||||
#define POP_FLASH_YELLOW 0
|
||||
#define POP_FLASH_RED 1
|
||||
#define POP_FLASH_GREEN 2
|
||||
#define POP_FLASH_WHITE 3 /* тень: слияние и её смерть (уровень 12) */
|
||||
extern uint8_t pop_flash_color;
|
||||
|
||||
/* add_life (seg000:11EC): +1 к ПОТОЛКУ HP и долив до него. Наружу — ради
|
||||
* слияния с тенью (уровень 12, guards.c в другом банке): большая склянка
|
||||
* зовёт ту же ветку изнутри модуля. */
|
||||
void pop_add_life(void) __banked;
|
||||
|
||||
/* is_feather_fall: кадров, пока действует зелье медленного падения. Читают
|
||||
* play_seq (опкод JMP_IF_FEATHER — ветки stepfloat/bumpfloat в seqtbl) и
|
||||
* приложение (сброс на старте уровня, seg003:189). */
|
||||
@@ -127,6 +146,20 @@ extern uint8_t pop_leave_timer;
|
||||
extern uint8_t pop_loose_modif[30];
|
||||
void pop_loose_tick(void) __banked;
|
||||
void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
|
||||
/* Отдать НЕЗАКОНЧЕННЫЕ loose уходящей комнаты в персистентный room_modif +
|
||||
* список trob, чтобы они досчитались заочно (в оригинале loose и есть trob,
|
||||
* а do_trobs крутит список независимо от drawn_room). Звать ПЕРЕД любой
|
||||
* перезагрузкой комнаты, пока карта ещё описывает СТАРУЮ. */
|
||||
void pop_loose_leave_room(void) __banked;
|
||||
/* СТАРТ/РЕСТАРТ уровня: забыть фазы БЕЗ сдачи (load_level сносит всё сам).
|
||||
* Звать рядом с pop_trob_reset, до первого enter_room. */
|
||||
void pop_loose_forget(void) __banked;
|
||||
|
||||
/* Персонаж стоит НА тайле ворот, значит их решётку надо вернуть ПОВЕРХ него
|
||||
* (порт условия draw_tile_fore, seg008:0D15 — разбор в pop_map.c). Бары
|
||||
* рисуются в колонке ch->curr_col + 1: графика ворот принадлежит правому
|
||||
* краю своего тайла. Рисование — pop_fore_over_char (pop_bg.c). */
|
||||
uint8_t pop_gate_over_char(const pop_char_t *ch) __banked;
|
||||
|
||||
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
|
||||
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
|
||||
|
||||
@@ -28,6 +28,18 @@
|
||||
#include <fcntl.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): оригинал под один
|
||||
* и тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE. У нас так
|
||||
@@ -809,9 +821,17 @@ void pop_loose_bake_empty(int row, int col) __banked
|
||||
* куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе
|
||||
* подряд, и второй спавн затирал состояние первого — его пиксели оставались
|
||||
* висеть в воздухе (на каждой странице на своей y → мерцание), а щебень
|
||||
* появлялся только под одной плитой. 4 слота: больше одновременно падающих
|
||||
* плит в одной комнате практически не бывает (в оригинале 14). */
|
||||
#define MOB_MAX 4
|
||||
* появлялся только под одной плитой.
|
||||
*
|
||||
* Слотов СТОЛЬКО ЖЕ, СКОЛЬКО В ОРИГИНАЛЕ (14). Было 4 с обоснованием
|
||||
* «больше одновременно падающих плит в одной комнате практически не
|
||||
* бывает» — неверным ровно с уровня 13: check_fall_flo роняет ШЕСТЬ разом
|
||||
* (комнаты 23 и 16), а с учётом плит, которые эти куски сбивают по дороге
|
||||
* (loose_fall), в воздухе бывает и больше. Лишний кусок молча терялся
|
||||
* (`if (!m) return`), то есть тайл исчезал, а щебня на месте посадки не
|
||||
* появлялось — найдено прогоном 2026-08-13, комната 16, тайл (1,5).
|
||||
* Цена — 150 байт W2. */
|
||||
#define MOB_MAX 14
|
||||
typedef struct {
|
||||
uint8_t active; /* 0 нет; 1 падает */
|
||||
uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */
|
||||
@@ -823,7 +843,12 @@ typedef struct {
|
||||
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
|
||||
uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
|
||||
} mob_t;
|
||||
static mob_t mobs[MOB_MAX];
|
||||
/* НЕ static намеренно: статики банкового модуля в карту символов не
|
||||
* попадают, и разобрать зависший кадр по слотам (y каждого куска, порядок,
|
||||
* defer) в отладчике нечем — адреса просто нет. Снято 2026-08-13 для
|
||||
* разбора артефактов множественного падения; наружу таблица не объявлена и
|
||||
* никем не читается. */
|
||||
mob_t mobs[MOB_MAX];
|
||||
|
||||
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
|
||||
* который он лёг → приложение/pop_map ставит debris + do_knock. */
|
||||
@@ -837,7 +862,7 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
|
||||
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
|
||||
#define MOB_Y_NONE 0x7FFF
|
||||
|
||||
void pop_loose_mob_spawn(int row, int col) __banked
|
||||
static void mob_spawn(uint8_t room, int row, int col)
|
||||
{
|
||||
uint8_t i;
|
||||
mob_t *m = 0;
|
||||
@@ -850,7 +875,7 @@ void pop_loose_mob_spawn(int row, int col) __banked
|
||||
m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
|
||||
m->row = (int8_t)row;
|
||||
m->col = (int8_t)col;
|
||||
m->room = pop_t_room; /* curmob.room (add_mob, seg007:1010) */
|
||||
m->room = room; /* curmob.room (add_mob, seg007:1010) */
|
||||
m->speed = 0;
|
||||
m->active = 1;
|
||||
m->clean = 0;
|
||||
@@ -858,6 +883,55 @@ void pop_loose_mob_spawn(int row, int col) __banked
|
||||
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
|
||||
}
|
||||
|
||||
/* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */
|
||||
void pop_loose_mob_spawn(int row, int col) __banked
|
||||
{
|
||||
mob_spawn(pop_t_room, row, col);
|
||||
}
|
||||
|
||||
/* Копия куска ОТ ПОЗИЦИИ УДАРА — порт хвоста loose_fall (seg007:1085):
|
||||
* curmob.y += 6; mob_down_a_row(); add_mob();
|
||||
* то есть новый кусок заводится не из тайла, а из места, где его сбили:
|
||||
* на 6 пикселей ниже и рядом ниже, со скоростью УЖЕ ПОДЕЛЁННОЙ пополам.
|
||||
* Через границу комнаты — как mob_down_a_row: y −= 192, ряд 0, комната снизу. */
|
||||
static void mob_spawn_copy(const mob_t *src)
|
||||
{
|
||||
uint8_t i;
|
||||
mob_t *m = 0;
|
||||
int16_t y = (int16_t)(src->y + 6);
|
||||
int8_t row = (int8_t)(src->row + 1);
|
||||
uint8_t room = src->room;
|
||||
|
||||
if (row >= 3) {
|
||||
y = (int16_t)(y - 192);
|
||||
row = 0;
|
||||
room = pop_room_link(room, 3 /* вниз */);
|
||||
if (!room) return; /* ушло из уровня — куску негде лететь */
|
||||
}
|
||||
for (i = 0; i < MOB_MAX; i++)
|
||||
if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; }
|
||||
if (!m) return;
|
||||
m->x = src->x;
|
||||
m->y = y;
|
||||
m->row = row;
|
||||
m->col = src->col;
|
||||
m->room = room;
|
||||
m->speed = src->speed; /* уже half (loose_fall делит ДО add_mob) */
|
||||
m->active = 1;
|
||||
m->clean = 0;
|
||||
m->defer = 0;
|
||||
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
|
||||
}
|
||||
|
||||
/* Отрыв куска в ЧУЖОЙ комнате: плита, которую Кид расшатал и убежал, всё
|
||||
* равно проваливается (animate_loose в pop_trob крутит её trob по своей
|
||||
* комнате, как do_trobs оригинала). Летит и садится кусок дальше сам —
|
||||
* mob_tick_one давно умеет чужую комнату (mob_tile_at / pop_loose_exit). */
|
||||
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked
|
||||
{
|
||||
mob_spawn(room, row, col);
|
||||
}
|
||||
|
||||
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
|
||||
* с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
|
||||
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
|
||||
@@ -905,6 +979,29 @@ static uint8_t mob_tile_at(mob_t *m)
|
||||
return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F);
|
||||
}
|
||||
|
||||
/* loose_fall (seg007:1082) — падающий кусок пробил ЕЩЁ ОДНУ расшатанную
|
||||
* плиту. Порт дословный, три действия В ОДНОМ КАДРЕ:
|
||||
* 1. налетевший кусок тормозится вдвое (`curmob.speed >>= 1`);
|
||||
* 2. сбитый тайл снимается с карты немедленно (`remove_loose`);
|
||||
* 3. на месте удара заводится ВТОРОЙ кусок — копия налетевшего, сдвинутая
|
||||
* на 6 пикселей и ряд вниз (`curmob.y += 6; mob_down_a_row; add_mob`).
|
||||
* То есть в столбце дальше летят ДВА куска, и это правильное поведение
|
||||
* оригинала, а не дефект.
|
||||
*
|
||||
* Здесь стоял обходной путь: плите ставилась фаза «падает следующим кадром»,
|
||||
* а всю работу делал pop_loose_tick. Цена оценивалась как «один кадр
|
||||
* задержки» и оказалась выше: защита «один сигнал pop_loose_fell на кадр»
|
||||
* придерживает провал, пока главный цикл не разберёт предыдущий, и на 13-м
|
||||
* уровне (плиты сыплются пачкой) вторая плита появлялась через несколько
|
||||
* кадров — уже после того, как первая улетела с экрана, — да ещё и с высоты
|
||||
* покоя тайла, а не от места удара (найдено прогоном 2026-08-13). */
|
||||
static void mob_break_loose(mob_t *m)
|
||||
{
|
||||
m->speed >>= 1;
|
||||
pop_loose_remove(m->room, (uint8_t)(m->row * 10 + m->col));
|
||||
mob_spawn_copy(m);
|
||||
}
|
||||
|
||||
static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
{
|
||||
uint8_t here;
|
||||
@@ -921,6 +1018,19 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
* на одной из двух страниц дабл-буфера — «застрявшая плита», видимая
|
||||
* через кадр (BUG-MOB-STALE-PAGE). */
|
||||
if (m->prev_y[pg] != MOB_Y_NONE) {
|
||||
/* Ширина/высота — по СЛЕДУ, а не по спрайту. Спрайт куска целиком
|
||||
* влезает в 64x32 (41: 32x13 с y-3, 42: 26x15 с x+32,y-1, 43: 32x3),
|
||||
* но mob_render рисует ещё и ЦЕЛЫЙ соседний тайл поверх правой части
|
||||
* (окклюзия грани пола), причём в GFX_BANK_SPRITE — то есть мимо
|
||||
* теневой копии фона. Всё, что тот тайл нарисовал вне коридора
|
||||
* heal'а, не стиралось никогда: кусок улетал, а в соседнем столбце
|
||||
* оставался обрезок пола (найдено прогоном 2026-08-13). Коридор
|
||||
* расширен до площади, которую реально может тронуть эта
|
||||
* перерисовка: столбец соседа по ширине и ряд по высоте.
|
||||
*
|
||||
* ЗАДЕЛ: правильнее не перерисовывать соседа вовсе, а КЛИПОВАТЬ
|
||||
* правую часть куска — оригинал так и делает (add_mob_to_objtable,
|
||||
* seg007:1161: clip.right = 40). Тогда коридор вернётся к 64x32. */
|
||||
pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
|
||||
m->prev_y[pg] = MOB_Y_NONE;
|
||||
}
|
||||
@@ -929,11 +1039,19 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
if (m->speed < 29) m->speed += 3;
|
||||
m->y += m->speed;
|
||||
/* move_loose (seg007:1126): низ куска пересёк границу своего ряда —
|
||||
* смотрим тайл этого ряда. Пусто/loose — валится дальше (порт
|
||||
* loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
|
||||
* пол — приземление (loose_land: debris + do_knock). */
|
||||
* смотрим тайл этого ряда. Пусто/loose — валится дальше, пол —
|
||||
* приземление (loose_land: debris + do_knock). Через LOOSE кусок именно
|
||||
* ПРОЛЕТАЕТ, как через пустоту, — но по дороге СРЫВАЕТ её (loose_fall,
|
||||
* seg007:1082).
|
||||
*
|
||||
* Здесь раньше стояло «порт loose_fall не нужен: в ряду ниже её нет» —
|
||||
* допущение, верное для уровней 1-11 и сломавшееся на 13-м: комната 16,
|
||||
* плиты (1,6) и (1,7), на которые сверху сыплется гряда из комнаты 1.
|
||||
* Куски пролетали сквозь них правильно, а сами плиты оставались целыми и
|
||||
* нарисованными (найдено прогоном 2026-08-13). */
|
||||
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
|
||||
uint8_t t = mob_tile_at(m);
|
||||
if (t == 11) mob_break_loose(m);
|
||||
if (t == 0 || t == 11) {
|
||||
/* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда —
|
||||
* в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает
|
||||
@@ -953,6 +1071,14 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
}
|
||||
here = (uint8_t)(m->room == pop_t_room);
|
||||
}
|
||||
} else if (!here && pop_loose_exit) {
|
||||
/* Сигнал прошлой посадки в чужой комнате ещё не разобран (он
|
||||
* ОДИН на кадр) — держим кусок на границе ряда и садим следующим
|
||||
* кадром, как это уже сделано для pop_loose_landed ниже. Без
|
||||
* этого второй кусок затирал первый, и из нескольких плит,
|
||||
* улетевших в одну дыру, щебень оставляла только одна (найдено
|
||||
* прогоном 2026-08-13, уровень 13). */
|
||||
m->y = MOB_Y_BOUND[m->row + 1];
|
||||
} else if (!here) {
|
||||
/* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, но щебень и
|
||||
* КНОПКА под куском обязаны сработать — сигналим главному циклу
|
||||
@@ -996,9 +1122,47 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */
|
||||
else if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col);
|
||||
else over = (m->y > pop_cd[POP_CH_KID].fpy);
|
||||
if (over) { m->defer = 1; return; }
|
||||
/* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок
|
||||
* пойдёт. Пока heal и вывод шли в одной итерации, heal СЛЕДУЮЩЕГО
|
||||
* куска срезал верхнюю кромку уже нарисованного предыдущего: на 13-м
|
||||
* уровне куски идут лесенкой в соседних столбцах с разницей ~20 px
|
||||
* по y, и перекрытие гарантировано (артефакт «чёрный бар», найден
|
||||
* прогоном 2026-08-13, комната 23). Теперь сперва тикают ВСЕ куски
|
||||
* (то есть heal'ятся все), и только потом рисуются. */
|
||||
m->defer = over;
|
||||
}
|
||||
mob_render(m, pg);
|
||||
}
|
||||
|
||||
/* Один проход отрисовки кусков. want_defer: 0 — те, что под Kid (идут сразу
|
||||
* после фона), 1 — те, что поверх него (после персонажей).
|
||||
*
|
||||
* Порядок внутри прохода — ПО y, но ПО УБЫВАНИЮ: сперва нижние, последним
|
||||
* верхний. Это НЕ описка и не общее правило «ниже = поверх»: сравнение двух
|
||||
* объектов в оригинале (compare_curr_objs, seg008:1572) разводит два случая,
|
||||
*
|
||||
* оба объекта — падающие плиты (тип 0x80): y1 < y2 → по УБЫВАНИЮ
|
||||
* любая другая пара: y1 > y2 → по возрастанию
|
||||
*
|
||||
* то есть между СОБОЙ куски укладываются наоборот, чем всё остальное: верхний
|
||||
* ложится поверх нижнего. Видно на паре из loose_fall (сбитая плита летит в
|
||||
* 6 пикселях под налетевшей): при общем правиле нижняя перекрывала верхнюю,
|
||||
* и это поймал прогон 2026-08-13.
|
||||
*
|
||||
* Порядок «кусок против Кида» здесь ни при чём — его решает defer выше, и там
|
||||
* действует обычное правило. */
|
||||
static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
|
||||
{
|
||||
uint8_t order[MOB_MAX], n = 0, i, j;
|
||||
for (i = 0; i < MOB_MAX; i++) {
|
||||
mob_t *m = &mobs[i];
|
||||
if (!m->active || m->room != pop_t_room) continue;
|
||||
if ((uint8_t)(m->defer != 0) != want_defer) continue;
|
||||
for (j = n; j > 0 && mobs[order[j - 1]].y < m->y; j--)
|
||||
order[j] = order[j - 1];
|
||||
order[j] = i;
|
||||
n++;
|
||||
}
|
||||
for (i = 0; i < n; i++) mob_render(&mobs[order[i]], pg);
|
||||
}
|
||||
|
||||
void pop_loose_mob_tick(void) __banked
|
||||
@@ -1010,6 +1174,15 @@ void pop_loose_mob_tick(void) __banked
|
||||
* (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а
|
||||
* стереть себя ему всё равно надо будет. */
|
||||
if (mobs[i].active || mobs[i].clean)
|
||||
/* Метка «фон трогали вот здесь» обязана покрывать ВЕСЬ коридор
|
||||
* heal'а, а не габарит спрайта. Иначе персонаж, попавший в
|
||||
* расширенную часть коридора, стирается heal'ом, но перерисовать
|
||||
* себя не просит (pop_char_skip_mask его пропускает) — и просто
|
||||
* исчезает с экрана. Ровно это и случилось, когда коридор
|
||||
* расширили под транзитную перерисовку соседа (прогон
|
||||
* 2026-08-13: в комнате 23 после падения гряды пропадал Кид).
|
||||
* Запас сверху — на путь, пройденный за кадр (speed до 29): heal
|
||||
* стирает ПРОШЛУЮ позицию, она выше текущей. */
|
||||
pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64);
|
||||
mob_tick_one(&mobs[i], pg);
|
||||
}
|
||||
@@ -1042,6 +1215,24 @@ static void mob_render(mob_t *m, uint8_t pg)
|
||||
* границе рядов. Порт draw_mob (seg007:13E5). */
|
||||
int8_t r = pop_y_to_row((int16_t)m->y);
|
||||
int8_t rt = pop_y_to_row((int16_t)(m->y - 18));
|
||||
/* ОКНО = КОРИДОР HEAL ЭТОГО КУСКА, ни пикселем шире. draw_tile
|
||||
* рисует тайл ЦЕЛИКОМ (пол, грани, верх кладки ряда ниже — порядка
|
||||
* 64x63), причём в GFX_BANK_SPRITE, то есть мимо теневой копии фона.
|
||||
* Всё, что он клал за пределами коридора, не стиралось НИКОГДА:
|
||||
* кусок улетал, а в соседнем столбце оставался обрезок пола
|
||||
* (найдено прогоном 2026-08-13).
|
||||
*
|
||||
* Расширять под это heal НЕЛЬЗЯ: heal — самая дорогая операция
|
||||
* кадра, а на 13-м уровне таких кусков одновременно шесть. Поэтому
|
||||
* режем не heal под перерисовку, а перерисовку под heal — тем же
|
||||
* окном, которым fore-слой отсекает лишние тайлы. */
|
||||
pop_t_fclip_x0 = MOB_X0(m->x);
|
||||
pop_t_fclip_x1 = MOB_X0(m->x) + MOB_W;
|
||||
pop_t_fclip_y0 = m->y - 27 + POP_YOFF;
|
||||
pop_t_fclip_y1 = pop_t_fclip_y0 + 32;
|
||||
pop_t_fclip_ly0 = pop_t_fclip_y0; /* переворота у кусков нет */
|
||||
pop_t_fclip_ly1 = pop_t_fclip_y1;
|
||||
pop_t_fclip_on = 1;
|
||||
if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */
|
||||
draw_tile(r, m->col + 1);
|
||||
pop_t_clip_top = 0;
|
||||
@@ -1050,19 +1241,30 @@ static void mob_render(mob_t *m, uint8_t pg)
|
||||
draw_tile(rt, m->col + 1);
|
||||
pop_t_clip_top = 0;
|
||||
}
|
||||
pop_t_fclip_on = 0;
|
||||
}
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
/* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */
|
||||
pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30);
|
||||
}
|
||||
|
||||
/* Отрисовка кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed(): фон
|
||||
* кадра обязан быть полностью готов до любого спрайта.
|
||||
*
|
||||
* Раньше куски рисовались внутри pop_loose_mob_tick, то есть ДО разбора
|
||||
* пометок перерисовки, и перепечатка тайла ложилась поверх уже нарисованного
|
||||
* куска — окно задней стены, передняя грань пола (найдено прогоном
|
||||
* 2026-08-13, комнаты 16 и 23). */
|
||||
void pop_loose_mob_draw(void) __banked
|
||||
{
|
||||
mob_draw_pass(gfx_get_draw_page() & 1, 0);
|
||||
}
|
||||
|
||||
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
|
||||
* после отрисовки персонажей, до fore-слоя. */
|
||||
void pop_loose_mob_draw_over(void) __banked
|
||||
{
|
||||
uint8_t pg = gfx_get_draw_page() & 1, i;
|
||||
for (i = 0; i < MOB_MAX; i++)
|
||||
if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); }
|
||||
mob_draw_pass(gfx_get_draw_page() & 1, 1);
|
||||
}
|
||||
|
||||
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
|
||||
|
||||
@@ -81,3 +81,24 @@ void pop_dbg_trap(void) { pop_dbg_hits++; }
|
||||
|
||||
/* Зеркало уровня 4 — см. pop_state.h. */
|
||||
int8_t pop_jumped_mirror;
|
||||
|
||||
/* Тайл СОСЕДНЕЙ комнаты изменился, пока она не отрисована (плита, которую
|
||||
* Кид расшатал и бросил, провалилась заочно — animate_loose в pop_trob).
|
||||
* Кромочные колонки и у отрисовки (lcol_fg), и у коллизии (g_lcol/g_rcol) —
|
||||
* СНИМКИ на момент входа в комнату, поэтому их надо перечитать. Ставит
|
||||
* банк 6, разбирает главный цикл. */
|
||||
uint8_t pop_neigh_dirty;
|
||||
|
||||
/* seamless (seg000:900 + seg003:158): переход 12 → 13 идёт БЕЗ двери
|
||||
* уровня — уровень кончается фактом попадания в комнату 23, и следующий
|
||||
* начинается в комнате с тем же номером и тем же рядом. Флаг говорит
|
||||
* старту уровня «не сбрасывай HP» (и, когда появятся заставки, «не
|
||||
* показывай „LEVEL 13“»): Кид продолжает бежать, не заметив границы.
|
||||
* Ставит главный цикл, гасит pop_start_level. */
|
||||
uint8_t pop_seamless;
|
||||
|
||||
/* «Кид попал в комнату ЧИТОМ навигации» — держит бесшовный переход уровня.
|
||||
* Отладочная добавка, у оригинала телепорта между комнатами нет вовсе; см.
|
||||
* pop_dbg_roomnav и ../docs/impl_diff.md. Снимается первой же СМЕНОЙ
|
||||
* КОМНАТЫ ОБЫЧНЫМ ХОДОМ и стартом уровня. */
|
||||
uint8_t pop_nav_hold;
|
||||
|
||||
@@ -20,6 +20,19 @@ extern uint8_t pop_loose_landed;
|
||||
extern uint8_t pop_loose_exit;
|
||||
extern uint8_t pop_loose_exit_room;
|
||||
|
||||
/* Тайл СОСЕДНЕЙ (не отрисованной) комнаты изменился — перечитать кромки.
|
||||
* Подробности — в pop_state.c. */
|
||||
extern uint8_t pop_neigh_dirty;
|
||||
|
||||
/* Бесшовный переход между уровнями (seamless): подробности в pop_state.c.
|
||||
* Ставится главным циклом при попадании в комнату-выход, гасится стартом
|
||||
* уровня — он же единственный, кто на флаг смотрит. */
|
||||
extern uint8_t pop_seamless;
|
||||
|
||||
/* Держатель бесшовного перехода после ЧИТ-телепорта (см. pop_state.c).
|
||||
* Ставит pop_dbg_roomnav, снимает первая смена комнаты обычным ходом. */
|
||||
extern uint8_t pop_nav_hold;
|
||||
|
||||
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
|
||||
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
|
||||
extern uint8_t pop_immortal;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
|
||||
* нельзя звать из __banked-кода. */
|
||||
#include "pop_bg.h"
|
||||
#include "pop_tile.h" /* pop_palace — тип уровня как модификатор дыры */
|
||||
#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */
|
||||
#include "pop_kid.h" /* Kid.curr_row/alive — условие остановки чомпера */
|
||||
#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */
|
||||
@@ -31,6 +32,7 @@
|
||||
#define TILE_CHOMP 0x12 /* чомпер (челюсти) */
|
||||
#define TILE_TORCH 0x13
|
||||
#define TILE_TORCH_DEBRIS 0x1E
|
||||
#define TILE_EMPTY 0x00 /* remove_loose: на месте провалившейся плиты */
|
||||
|
||||
/* ---- Персистентное per-room modifier-состояние -------------------- */
|
||||
static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES];
|
||||
@@ -305,6 +307,54 @@ static void animate_spike(uint8_t *m, int8_t *type)
|
||||
*m = mod;
|
||||
}
|
||||
|
||||
/* animate_loose (seg007:0CBA) — для плиты в НЕ отрисованной комнате.
|
||||
*
|
||||
* ЗАЧЕМ. В оригинале loose-пол — обычный trob, а do_trobs крутит весь
|
||||
* список независимо от drawn_room: расшатанная плита доваливается, даже если
|
||||
* Кид уже убежал в соседнюю комнату. У нас фаза живёт в pop_loose_modif[30]
|
||||
* — массиве ТЕКУЩЕЙ комнаты, — и смена комнаты её обнуляла: плита,
|
||||
* пробежав по которой Кид уходил вправо, оставалась целой (уровень 10,
|
||||
* комната 2, тайл 2,9).
|
||||
*
|
||||
* Разделение обязанностей: пока комната отрисована, фазой владеет
|
||||
* pop_loose_tick (ему нужны перерисовка, звук, mob-порядок), и trob здесь
|
||||
* сразу снимается. На выходе из комнаты pop_loose_leave_room (pop_map.c)
|
||||
* сбрасывает фазу в ПЕРСИСТЕНТНЫЙ room_modif и заводит trob — дальше плита
|
||||
* досчитывает здесь, а на входе обратно pop_loose_reset забирает фазу
|
||||
* назад. Персистентный модификатор и есть curr_room_modif оригинала, так
|
||||
* что расхождения с SDLPoP нет — только двухуровневое хранение.
|
||||
*
|
||||
* Рисовать тут нечего (комната не на экране), поэтому от оригинала остаётся
|
||||
* ровно счёт фазы и провал: тайл в EMPTY + отрыв падающего куска. */
|
||||
static void animate_loose(uint8_t *m, int8_t *type, uint8_t room, uint8_t tp,
|
||||
uint8_t cur_room)
|
||||
{
|
||||
uint8_t ph;
|
||||
if (room == cur_room) { *type = -1; return; } /* хозяин — pop_loose_tick */
|
||||
ph = (uint8_t)(*m + 1);
|
||||
if (ph == 0) ph = 1; /* перескок нуля, см. pop_loose_tick */
|
||||
if (ph & 0x80) { /* тряска (do_knock): 0x80..0x83 */
|
||||
/* Спецкейс уровня 13 (seg007:823) — тот же, что в pop_loose_tick:
|
||||
* отложенный старт от check_fall_flo живёт в старшем бите, и гасить
|
||||
* его нельзя. Здесь он тоже нужен: гряда лежит в комнате СВЕРХУ, и
|
||||
* пока Кид не вошёл в комнату под ней, счёт идёт заочно. */
|
||||
if (pop_current_level == POP_LOOSE_TILES_LEVEL) { /* ждём молча */ }
|
||||
else if (ph >= 0x84) { ph = 0; *type = -1; }
|
||||
} else if (ph >= 11) { /* loose_floor_delay — провал */
|
||||
/* remove_loose (seg007:0EB8): тайл → EMPTY, модификатором пустой
|
||||
* клетки становится ТИП УРОВНЯ (по нему draw_tile_right выбирает
|
||||
* blueline у дыры). Тот же порядок, что в pop_loose_tick. */
|
||||
pop_level_set_tile(room, tp, TILE_EMPTY);
|
||||
/* Комната не на экране, но её колонка 9 ВИДНА в шве текущей и лежит
|
||||
* в кромочных снимках — попросить главный цикл их перечитать. */
|
||||
pop_neigh_dirty = 1;
|
||||
pop_loose_mob_spawn_at(room, TP_ROW[tp], TP_COL[tp]);
|
||||
ph = pop_palace;
|
||||
*type = -1;
|
||||
}
|
||||
*m = ph;
|
||||
}
|
||||
|
||||
/* ---- Кнопки/ворота (seg007) --------------------------------------- *
|
||||
* Таблицы связей LINKLOC/LINKMAP — в pop_level (мутабельная копия в W2).
|
||||
* doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */
|
||||
@@ -574,6 +624,9 @@ void pop_process_trobs(uint8_t cur_room) __banked
|
||||
case TILE_GATE:
|
||||
animate_door(&mod[tp], &type);
|
||||
break;
|
||||
case TILE_LOOSE:
|
||||
animate_loose(&mod[tp], &type, room, tp, cur_room);
|
||||
break;
|
||||
case TILE_LEVELDOOR:
|
||||
animate_leveldoor(&mod[tp], &type, cur_room);
|
||||
break;
|
||||
|
||||
@@ -126,4 +126,29 @@
|
||||
* чтение 34 страниц пришлась на загрузку уровня, а не на глоток зелья. */
|
||||
#define POP_UPSIDE_LEVEL 9
|
||||
|
||||
/* Уровень ПАДАЮЩИХ ПЛИТ (custom->loose_tiles_*, seg000:1317 check_fall_flo).
|
||||
* При входе в комнату 23 или 16 тринадцатого уровня вся гряда loose ряда 2
|
||||
* комнаты СВЕРХУ получает ОТРИЦАТЕЛЬНУЮ фазу — отложенный старт, — и плиты
|
||||
* сыплются на Кида вразнобой. Комнаты сверху — 17 и 1 соответственно, и в
|
||||
* них ряд 2 колонок 2..7 действительно loose (сверено по res2013.bin,
|
||||
* ../docs/levels_12_15_plan.md §7.1). Диапазон 22..27 — надмножество: в
|
||||
* комнате 1 тайл 24 пустой, и он молча пропускается.
|
||||
*
|
||||
* Комната 23 у этого уровня ещё и СТАРТОВАЯ, то есть плиты сыплются с
|
||||
* первого кадра уровня — это его открывающая сцена, а не событие в
|
||||
* середине. */
|
||||
#define POP_LOOSE_TILES_LEVEL 13
|
||||
#define POP_LOOSE_TILES_ROOM1 23
|
||||
#define POP_LOOSE_TILES_ROOM2 16
|
||||
#define POP_LOOSE_TILES_FIRST 22 /* ряд 2, колонка 2 */
|
||||
#define POP_LOOSE_TILES_LAST 27 /* ряд 2, колонка 7 */
|
||||
|
||||
/* Бесшовный выход уровня (tbl_seamless_exit, play_level_2 seg000:900). У
|
||||
* оригинала это таблица по уровням, но заполнена она ровно одной парой:
|
||||
* уровень 12 кончается, как только Кид попал в комнату 23 — двери уровня у
|
||||
* него нет. Комната 23 у 13-го уровня СТАРТОВАЯ и совпадает геометрией
|
||||
* ряда, поэтому шва не видно (../docs/levels_12_15_plan.md §1.7). */
|
||||
#define POP_SEAMLESS_LEVEL 12
|
||||
#define POP_SEAMLESS_ROOM 23
|
||||
|
||||
#endif /* POP_TUNE_H */
|
||||
|
||||
@@ -355,6 +355,8 @@ int main(void)
|
||||
}
|
||||
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
|
||||
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
|
||||
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
|
||||
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
|
||||
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
|
||||
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
|
||||
@@ -396,6 +398,7 @@ int main(void)
|
||||
pop_next_level = (uint8_t)(pop_current_level + 1);
|
||||
}
|
||||
else if (down) { /* L2: переход в комнату снизу — продолжает падать */
|
||||
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
|
||||
enter_room(down); /* координаты Kid уже репроецированы pop_map */
|
||||
gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */
|
||||
} else { /* выпал из уровня (нет комнаты снизу) — рестарт */
|
||||
@@ -424,9 +427,20 @@ int main(void)
|
||||
pop_checkpoint = 1;
|
||||
hitp_beg_lev = hitp_max;
|
||||
}
|
||||
/* Соседи чекпойнта по leave_room (seg002:0490): в ветке ухода
|
||||
* ВПРАВО (pop_leave_dir == 2) — sword_disappears + meet_Jaffar,
|
||||
* в ветке ВЛЕВО — Jaffar_exit. Комната во всех трёх ещё СТАРАЯ,
|
||||
* как и в оригинале (goto_other_room идёт после). */
|
||||
if (pop_leave_dir_prev == 2) {
|
||||
pop_sword_disappears();
|
||||
pop_meet_jaffar();
|
||||
} else if (left) {
|
||||
pop_jaffar_exit();
|
||||
}
|
||||
if (nb) {
|
||||
/* side для pop_guard_follow — то же направление, по которому
|
||||
* ищем соседа (roomleave_result оригинала). */
|
||||
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
|
||||
enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1));
|
||||
gfx_set_draw_page(back);
|
||||
}
|
||||
@@ -438,6 +452,30 @@ int main(void)
|
||||
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
|
||||
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
|
||||
/* process_trobs — рисования внутри нет) */
|
||||
if (pop_neigh_dirty) {
|
||||
/* Тайл СОСЕДА изменился заочно (провалилась брошенная плита —
|
||||
* animate_loose). Обе кромочные копии — снимки на момент входа
|
||||
* в комнату, обновляем обе: g_lcol/g_rcol (коллизия у шва) и
|
||||
* lcol_fg (отрисовка: колонка 9 левого соседа видна в col0
|
||||
* через draw_tile_anim_right, pop_room.c). Без второй шов
|
||||
* продолжал рисовать плиту, которой уже нет — LOOSE-ROOM-CHANGE.
|
||||
* Событие редкое, поэтому по сигналу, а не опросом каждый кадр. */
|
||||
uint8_t lft = pop_room_link(cur_room, 0);
|
||||
pop_neigh_dirty = 0;
|
||||
pop_map_refresh_edges();
|
||||
if (lft) {
|
||||
uint8_t i;
|
||||
pop_level_access_begin();
|
||||
for (i = 0; i < 3; i++) {
|
||||
uint8_t c = pop_level_tile_raw(lft, (uint8_t)(9 + i * 10));
|
||||
if (c == (uint8_t)(lcol_fg[i] & 0x1F)) continue;
|
||||
lcol_fg[i] = c;
|
||||
seam_rows |= (uint8_t)(1 << i);
|
||||
seam_redraw = 2; /* обе страницы дабл-буфера */
|
||||
}
|
||||
pop_level_access_end();
|
||||
}
|
||||
}
|
||||
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
|
||||
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
|
||||
{ /* ворота соседа слева (room8) видны в col0 — */
|
||||
@@ -471,6 +509,18 @@ int main(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Бесшовный выход уровня 12 (play_level_2, seg000:900): двери уровня
|
||||
* у него нет — уровень кончается ФАКТОМ попадания в комнату 23.
|
||||
* Условие «уровень ещё не сменён» обязательно: комната 23 есть и у
|
||||
* 13-го, причём она там СТАРТОВАЯ, и без гарда переход зациклился бы
|
||||
* (12 → 13 → 14 в первом же кадре нового уровня). */
|
||||
if (pop_current_level == POP_SEAMLESS_LEVEL &&
|
||||
pop_next_level == pop_current_level &&
|
||||
!pop_nav_hold && /* не по чит-телепорту, см. pop_state.c */
|
||||
kid_room == POP_SEAMLESS_ROOM) {
|
||||
pop_next_level = (uint8_t)(pop_current_level + 1);
|
||||
pop_seamless = 1;
|
||||
}
|
||||
if (pop_next_level != pop_current_level) {
|
||||
/* Смена уровня целиком — в банке (pop_level_switch): грузится
|
||||
* файл, спрайты соперника и тайлсет. Раз на уровень. */
|
||||
@@ -550,6 +600,7 @@ int main(void)
|
||||
* HP — красный, перо (медленное падение) — зелёный. */
|
||||
if (pop_flash_color == POP_FLASH_RED) flash_bg(255, 0, 0);
|
||||
else if (pop_flash_color == POP_FLASH_GREEN) flash_bg(85, 255, 85);
|
||||
else if (pop_flash_color == POP_FLASH_WHITE) flash_bg(255, 255, 255);
|
||||
else flash_bg(255, 255, 85);
|
||||
pop_flash_time--;
|
||||
if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW;
|
||||
@@ -567,6 +618,11 @@ int main(void)
|
||||
* пролетел сквозь зеркало, тень должна родиться ДО того, как слот
|
||||
* соперника пойдёт рисоваться, иначе кадр её проглотит. */
|
||||
pop_check_mirror();
|
||||
/* Падающие плиты, идущие ПОД Kid. Здесь, а не внутри pop_loose_tick:
|
||||
* фон кадра (перепечатки тайлов, шов) обязан быть готов ДО любого
|
||||
* спрайта, иначе перепечатка ложится поверх уже нарисованного куска —
|
||||
* так окно задней стены и передняя грань пола оказывались на плите. */
|
||||
pop_loose_mob_draw();
|
||||
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
|
||||
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char
|
||||
* (pop_cdraw.c), различает их только слот. */
|
||||
|
||||
@@ -109,7 +109,15 @@ void pop_start_level(void) __banked
|
||||
pop_level_reset_tiles();
|
||||
pop_level_reset_guards();
|
||||
pop_guard_reset(); /* start_level: Guard.direction = none */
|
||||
/* united_with_shadow = 0 (seg003:95) — состояние ПЕРСОНАЖА уровня, а не
|
||||
* комнаты: pop_guard_reset зовётся и при каждом входе в комнату (из
|
||||
* pop_guard_enter), поэтому гасить слияние надо здесь. */
|
||||
pop_united_shadow = 0;
|
||||
pop_shadow_init = 0;
|
||||
pop_guard_notice_timer = 0; /* фора Джафара — не переживает рестарт */
|
||||
pop_nav_hold = 0; /* старт уровня снимает и держатель чита */
|
||||
pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */
|
||||
pop_loose_forget(); /* ...и фазы плит — уровень восстановлен целиком */
|
||||
/* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в
|
||||
* комнату-чекпойнт после респавна остался бы непройденным. */
|
||||
if (chkp)
|
||||
@@ -155,7 +163,11 @@ void pop_start_level(void) __banked
|
||||
/* play_level (seg003:106): have_sword = level >= have_sword_from_level
|
||||
* (=2) — начиная со второго уровня меч у Кида уже есть. */
|
||||
pop_have_sword = (uint8_t)(pop_current_level >= 2);
|
||||
pop_kid_hp_reset();
|
||||
/* seamless (seg003:158): пришли БЕЗ двери уровня, прямо на бегу — HP не
|
||||
* трогаем, оно переезжает как есть. Флаг одноразовый: у оригинала его
|
||||
* гасит заставка «LEVEL N», которую этот же переход и пропускает. */
|
||||
if (pop_seamless) { pop_seamless = 0; pop_kid_hp_carry(); }
|
||||
else pop_kid_hp_reset();
|
||||
}
|
||||
|
||||
/* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- *
|
||||
@@ -234,6 +246,16 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked
|
||||
holding_sword = 0;
|
||||
offguard = 0;
|
||||
guard_refrac = 0;
|
||||
|
||||
/* ПРОВАЛ НА СЛЕДУЮЩИЙ УРОВЕНЬ ПО ЧИТУ (та же болезнь, что выше).
|
||||
* Уровень 12 кончается ФАКТОМ попадания в комнату 23 — двери у него нет
|
||||
* (pop_seamless, seg000:0900). А навигация перебирает комнаты ПО
|
||||
* НОМЕРУ, то есть любой обход рано или поздно наступает на 23-ю, и
|
||||
* уровень молча меняется на 13-й: комнату 23 двенадцатого уровня читом
|
||||
* не посмотреть в принципе, а прогон «съезжает» на соседний уровень
|
||||
* незаметно для проверяющего (поймано 2026-08-13 на первом же прогоне).
|
||||
* Держим триггер, пока Кид не сменит комнату НОГАМИ. */
|
||||
pop_nav_hold = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -276,6 +298,10 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
|
||||
* enter звать нельзя: он уже перенесён и живёт дальше как есть. */
|
||||
uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0);
|
||||
if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */
|
||||
/* ...и так же запомнить НЕДОСЧИТАННЫЕ loose: пока карта ещё описывает
|
||||
* СТАРУЮ комнату, фаза уезжает в персистентный room_modif + trob, и
|
||||
* плита доваливается заочно (см. pop_loose_leave_room / animate_loose). */
|
||||
pop_loose_leave_room();
|
||||
uint8_t *room_mod;
|
||||
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
|
||||
pop_map_set(room_fg);
|
||||
@@ -317,6 +343,7 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
|
||||
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
|
||||
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
|
||||
pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */
|
||||
pop_check_fall_flo(); /* спецсобытие ур.13: гряда плит сверху */
|
||||
/* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»-
|
||||
* уровнях забирает слот соперника себе: вернула 1 — стража поднимать
|
||||
* НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда
|
||||
|
||||
@@ -49,6 +49,12 @@ OBJS_char := $(OBJS_phys)
|
||||
# (pop_check_mouse) и общей физикой соперника — то есть ровно то, что в
|
||||
# остальных наборах заменено ручным прогоном play_guard_frame.
|
||||
OBJS_mouse := $(OBJS_phys) build/eng_guards.rel
|
||||
# t_shadow — спецсобытия уровня 12 (встреча с тенью, общий урон, слияние).
|
||||
# guards.c нужен по той же причине, что и мыши: check_shadow/
|
||||
# check_killed_shadow/sword_disappears живут в нём.
|
||||
OBJS_shadow := $(OBJS_phys) build/eng_guards.rel
|
||||
# t_jaffar — спецсобытия уровня 13 (встреча, победа, выход, падающая гряда).
|
||||
OBJS_jaffar := $(OBJS_phys) build/eng_guards.rel
|
||||
|
||||
include $(TESTKIT)/host-tests.mk
|
||||
|
||||
|
||||
@@ -46,12 +46,18 @@ void sc_room(const uint8_t *fg, uint8_t room)
|
||||
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir)
|
||||
{
|
||||
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
|
||||
/* kid_init комнату не трогает (в продукте её ставит enter_room_side), а
|
||||
* сцена обязана быть согласованной: логика, спрашивающая «персонаж в
|
||||
* ОТРИСОВАННОЙ комнате?» (pop_gate_over_char), иначе всегда отвечает
|
||||
* «нет». Тест, которому нужна другая комната, задаёт её сам. */
|
||||
Kid.room = tk_start_room;
|
||||
pop_kid_hp_reset();
|
||||
}
|
||||
|
||||
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir)
|
||||
{
|
||||
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
|
||||
Kid.room = tk_start_room;
|
||||
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */
|
||||
/* Колонку пересчитываем В ОКНЕ Char: determine_col — часть физики, а та
|
||||
* работает с активным персонажем (порт load_fram_det_col). */
|
||||
|
||||
@@ -223,12 +223,12 @@ uint8_t pop_level_guard_hp(uint8_t level) __banked { (void)level; return 3; }
|
||||
/* Тайлы «уровня» для соседних комнат: pop_map_set_edges забирает fg соседей
|
||||
* слева/справа сам (кэш на входе в комнату), значит тесту нужен источник.
|
||||
* Заполняет сам тест — по умолчанию комнат нет и всё стена. */
|
||||
uint8_t tk_room_fg[TK_ROOMS][30];
|
||||
uint8_t tk_room_fg[TK_ROOMS + 1][30]; /* индекс = НОМЕР комнаты (1..TK_ROOMS) */
|
||||
void pop_level_access_begin(void) { }
|
||||
void pop_level_access_end(void) { }
|
||||
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
|
||||
{
|
||||
if (room == 0 || room >= TK_ROOMS || tilepos >= 30) return 20; /* стена */
|
||||
if (room == 0 || room > TK_ROOMS || tilepos >= 30) return 20; /* стена */
|
||||
return tk_room_fg[room][tilepos];
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ uint8_t pop_level_tile(uint8_t room, uint8_t tilepos)
|
||||
* пор, как do_pickup правит страницу уровня сразу (BUG-POTION-STRIPE). */
|
||||
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
|
||||
{
|
||||
if (room == 0 || room >= TK_ROOMS || tilepos >= 30) return;
|
||||
if (room == 0 || room > TK_ROOMS || tilepos >= 30) return;
|
||||
tk_room_fg[room][tilepos] = tile;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,11 @@ uint8_t tk_log_count(uint8_t kind);
|
||||
|
||||
|
||||
/* Состояние стражей по комнатам для follow_guard (см. stubs.c). */
|
||||
#define TK_ROOMS 8
|
||||
/* Комнат в таблице «уровня». 24 = столько же, сколько в настоящем уровне
|
||||
* PoP: спецсобытия поздних уровней адресуют комнаты по их РЕАЛЬНЫМ номерам
|
||||
* (меч тени лежит в 15-й, уход за ним — из 18-й), и обрезать их до восьми
|
||||
* значило бы тестировать не тот адрес. Цена — 720 байт тестового ОЗУ. */
|
||||
#define TK_ROOMS 24
|
||||
extern uint8_t tk_guard_room; /* комната, где страж есть «по данным уровня» */
|
||||
extern uint8_t tk_gs_tile[TK_ROOMS];
|
||||
extern uint16_t tk_gs_seq[TK_ROOMS];
|
||||
@@ -52,6 +56,6 @@ void tk_guard_state_clear(void);
|
||||
extern uint8_t tk_link_down;
|
||||
|
||||
/* fg комнат «уровня» — источник для кэша соседей в pop_map_set_edges. */
|
||||
extern uint8_t tk_room_fg[TK_ROOMS][30];
|
||||
extern uint8_t tk_room_fg[TK_ROOMS + 1][30]; /* индекс = номер комнаты */
|
||||
|
||||
#endif
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
#define E 0 /* empty */
|
||||
#define F 1 /* floor */
|
||||
#define G 4 /* gate (portcullis) */
|
||||
#define W 20 /* wall */
|
||||
|
||||
/* Ровный пол на ряду 1 (как room_flat в t_phys). */
|
||||
@@ -47,6 +48,13 @@ static const uint8_t room_drop2[30] = {
|
||||
W, F, F, F, F, F, F, F, F, F,
|
||||
};
|
||||
|
||||
/* Решётка ворот в (2,6) — сцена под окклюзию персонажа, стоящего в проёме. */
|
||||
static const uint8_t room_gate[30] = {
|
||||
W, E, E, E, E, E, E, E, E, E,
|
||||
W, E, E, E, E, E, E, E, E, E,
|
||||
W, F, F, F, F, F, G, F, F, F,
|
||||
};
|
||||
|
||||
/* ---- take_hp: кому достаётся урон ----------------------------------- */
|
||||
|
||||
TC_TEST(char_take_hp_goes_to_active)
|
||||
@@ -434,6 +442,57 @@ TC_TEST(char_skeleton_falls_out_elsewhere)
|
||||
tc_check(Guard.charid, 0, "скелет убран, как обычный выпавший", __LINE__);
|
||||
}
|
||||
|
||||
/* ---- Окклюзия воротами (GATE-FORE-KID) ------------------------------ *
|
||||
* Персонаж, стоящий В ПРОЁМЕ ворот, обязан уходить ЗА решётку: оригинал
|
||||
* кладёт её бары в foretable (draw_tile_fore, seg008:0D15). Решение —
|
||||
* pop_gate_over_char; сама отрисовка живёт в слое фона и в харнесс не
|
||||
* линкуется, поэтому проверяем именно РЕШЕНИЕ. */
|
||||
|
||||
TC_TEST(char_gate_covers_char_standing_in_it)
|
||||
{
|
||||
sc_room(room_gate, 1);
|
||||
sc_kid_at(6, 2, 0); /* ровно на тайле ворот */
|
||||
pop_loadkid();
|
||||
TC_TRUE(pop_gate_over_char(&Char));
|
||||
}
|
||||
|
||||
TC_TEST(char_gate_ignores_neighbour_tiles)
|
||||
{
|
||||
sc_room(room_gate, 1);
|
||||
sc_kid_at(5, 2, 0); /* колонка слева от ворот */
|
||||
pop_loadkid();
|
||||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||||
sc_kid_at(7, 2, 0); /* и справа */
|
||||
pop_loadkid();
|
||||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||||
}
|
||||
|
||||
TC_TEST(char_gate_ignores_other_room)
|
||||
{
|
||||
/* Персонаж той же клетки, но ДРУГОЙ комнаты (страж, оставшийся в
|
||||
* соседней): его решётка текущей комнаты не касается. */
|
||||
sc_room(room_gate, 1);
|
||||
sc_kid_at(6, 2, 0);
|
||||
pop_loadkid();
|
||||
Char.room = 5;
|
||||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||||
}
|
||||
|
||||
TC_TEST(char_gate_never_in_last_column)
|
||||
{
|
||||
/* Колонка 9: бары легли бы в колонку 10, то есть в СОСЕДНЮЮ комнату —
|
||||
* у оригинала это отсекает `Kid.room != room_R`. */
|
||||
static const uint8_t room_gate9[30] = {
|
||||
W, E, E, E, E, E, E, E, E, E,
|
||||
W, E, E, E, E, E, E, E, E, E,
|
||||
W, F, F, F, F, F, F, F, F, G,
|
||||
};
|
||||
sc_room(room_gate9, 1);
|
||||
sc_kid_at(9, 2, 0);
|
||||
pop_loadkid();
|
||||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
sc_init();
|
||||
@@ -457,4 +516,8 @@ void main(void)
|
||||
TC_RUN(char_tiles_resolve_across_rooms);
|
||||
TC_RUN(char_skeleton_reappears_below);
|
||||
TC_RUN(char_skeleton_falls_out_elsewhere);
|
||||
TC_RUN(char_gate_covers_char_standing_in_it);
|
||||
TC_RUN(char_gate_ignores_neighbour_tiles);
|
||||
TC_RUN(char_gate_ignores_other_room);
|
||||
TC_RUN(char_gate_never_in_last_column);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -211,13 +211,44 @@ TC_TEST(phys_loose_floor_breaks)
|
||||
/* Журнал заглушек: плита обязана была отвалиться (кусок отрывается —
|
||||
* pop_loose_mob_spawn) и попросить перерисовку. Это уже не
|
||||
* характеризация, а содержательная проверка — заглушка-пустышка её не
|
||||
* дала бы. Через trob loose у нас НЕ идёт (только пики/ворота), у него
|
||||
* свой pop_loose_modif — поэтому спрашиваем именно про кусок. */
|
||||
* дала бы. Пока плита ЖИВЁТ в отрисованной комнате, фазой владеет
|
||||
* pop_loose_modif, а не trob — поэтому спрашиваем именно про кусок. */
|
||||
TC_TRUE(tk_log_count(TK_MOB_SPAWN) > 0);
|
||||
TC_TRUE(tk_log_count(TK_REDRAW) > 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
TC_TEST(phys_loose_survives_room_change)
|
||||
{
|
||||
/* GUARD против бага «плита не успевает провалиться»: Кид расшатал плиту
|
||||
* и ушёл в соседнюю комнату — отсчёт обязан ПЕРЕЖИТЬ смену комнаты.
|
||||
* В оригинале терять нечего (loose — обычный trob в общем списке, фаза
|
||||
* в curr_room_modif); у нас фаза живёт в pop_loose_modif[] текущей
|
||||
* комнаты, и её сдаёт/забирает пара leave_room + reset. */
|
||||
uint8_t pos = 1 * 10 + 4; /* loose-плита сцены room_loose */
|
||||
uint8_t phase = 3; /* середина отсчёта до провала (1..10) */
|
||||
|
||||
/* Саму анимацию проверяет phys_loose_floor_breaks выше; здесь нужна
|
||||
* ровно передача фазы, поэтому ставим её явно — прогон бы привязал тест
|
||||
* к тому, на каком кадре Кид добежит до плиты. */
|
||||
sc_room(room_loose, 1);
|
||||
pop_loose_modif[pos] = phase;
|
||||
|
||||
/* Уход из комнаты: фаза уезжает в room_modif + trob... */
|
||||
tk_log_clear();
|
||||
pop_loose_leave_room();
|
||||
TC_TRUE(tk_log_count(TK_TROB) > 0); /* плита сдана в общий список */
|
||||
|
||||
/* ...и возвращается при входе обратно — а не обнуляется, как раньше. */
|
||||
pop_loose_reset();
|
||||
TC_EQ(pop_loose_modif[pos], phase);
|
||||
|
||||
/* Старт уровня, наоборот, обязан фазу ЗАБЫТЬ (load_level восстанавливает
|
||||
* уровень целиком) — иначе дрожь протекала бы в следующую жизнь. */
|
||||
pop_loose_forget();
|
||||
TC_EQ(pop_loose_modif[pos], 0);
|
||||
}
|
||||
|
||||
TC_TEST(phys_running_jump_over_3tile_gap)
|
||||
{
|
||||
/* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок.
|
||||
@@ -295,6 +326,7 @@ void main(void)
|
||||
TC_RUN(phys_jump_up_nothing_above);
|
||||
TC_RUN(phys_run_off_ledge);
|
||||
TC_RUN(phys_loose_floor_breaks);
|
||||
TC_RUN(phys_loose_survives_room_change);
|
||||
TC_RUN(phys_running_jump_over_3tile_gap);
|
||||
TC_RUN(phys_feather_fall_is_slow_and_harmless);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -27,8 +27,9 @@ from pop_pack_kid import transpose_cols, vflip_page
|
||||
# остальных наборов curr_guard_color = 0 (seg002:186) — значит цвет берётся
|
||||
# из ФАЙЛА набора (res750.pal), и подменять его не надо.
|
||||
SETS = {
|
||||
"GUARD": ("GUARD", "guard", None),
|
||||
"SKEL": ("SKEL", "skel", "res750.pal"),
|
||||
"GUARD": ("GUARD", "guard", None),
|
||||
"SKEL": ("SKEL", "skel", "res750.pal"),
|
||||
"VIZIER": ("VIZIER", "vizier", "res750.pal"),
|
||||
}
|
||||
SET_NAME = "GUARD"
|
||||
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD")
|
||||
@@ -42,7 +43,8 @@ MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
|
||||
|
||||
|
||||
def select_set(name):
|
||||
"""Переключить упаковщик на набор (GUARD/SKEL): каталоги и число кадров."""
|
||||
"""Переключить упаковщик на набор (GUARD/SKEL/VIZIER): каталоги и число
|
||||
кадров."""
|
||||
global SET_NAME, GUARD_DIR, OUT_DIR, MAX_ID
|
||||
if name not in SETS:
|
||||
B.die("неизвестный набор %s (есть: %s)" % (name, ", ".join(SETS)))
|
||||
@@ -142,19 +144,23 @@ def write_pal_header(out_path):
|
||||
"};",
|
||||
"",
|
||||
]
|
||||
# Палитра скелета — из ЕГО набора (res750.pal), а не из res10.bin: на
|
||||
# уровне 3 curr_guard_color = 0, оригинал палитру не подменяет вовсе.
|
||||
# Палитры наборов со СВОИМ цветом (скелет, визирь) — из их res750.pal, а
|
||||
# не из res10.bin: на таких уровнях curr_guard_color = 0 (seg002:186), и
|
||||
# оригинал палитру не подменяет вовсе.
|
||||
saved = SET_NAME
|
||||
select_set("SKEL")
|
||||
skel = set_palette()
|
||||
select_set(saved)
|
||||
if skel:
|
||||
for name, cname, human in (("SKEL", "pop_skel_pal", "Скелет"),
|
||||
("VIZIER", "pop_vizier_pal", "Джафар")):
|
||||
select_set(name)
|
||||
pal = set_palette()
|
||||
select_set(saved)
|
||||
if not pal:
|
||||
continue
|
||||
vals = []
|
||||
for (r, g, b) in skel:
|
||||
for (r, g, b) in pal:
|
||||
vals += [B.scale6to8(b), B.scale6to8(g), B.scale6to8(r), 0]
|
||||
lines += [
|
||||
"/* Скелет (SKEL/res750.pal): свой набор — свой цвет. */",
|
||||
"static const uint8_t pop_skel_pal[64] = {",
|
||||
"/* %s (%s/res750.pal): свой набор — свой цвет. */" % (human, name),
|
||||
"static const uint8_t %s[64] = {" % cname,
|
||||
" " + ",".join("0x%02X" % v for v in vals) + ",",
|
||||
"};",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user