# Что осталось на уровнях 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`.