Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
25 KiB
Что осталось на уровнях 12, 13, 14, 15 и 0
Статус: разбор по ../SDLPoP/src/, 2026-08-13. Продолжает
levels_plan.md (машинерия уровней и тайлсеты — уже
сделаны). Здесь только СПЕЦСОБЫТИЯ, которых у нас ещё нет.
Правило проекта: источник истины — SDLPoP; все ссылки ниже даны на функцию и строку, чтобы порт начинался с чтения, а не с гипотезы.
0. Что из этой области УЖЕ есть
Проверено грепом по SprPoP/:
| механика | где у нас | статус |
|---|---|---|
| слот соперника, общий на всех 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, sprpop.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)
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)
Самая содержательная функция уровня. Три режима:
- Первый кадр в комнате 15: пока Кид не подошёл (
Opp.x < 150) —shadow_initialized = 1; иначе тень ещё раз падает (do_init_shad). - Кид с мечом (
Char.sword >= sword_2_drawn) → тень дерётся обычнымautocontrol_guard_active(у нас есть,guards.c:535). Особый случай: если тень уже ранена (offguard != 0 && guard_refrac != 0) — она убирает меч (move_4_down). - Кид убрал меч → тень тоже убирает и идёт навстречу; на дистанции
< 10— СЛИЯНИЕ:
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)
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)
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)
if ((Guard.charid != charid_1_shadow || current_level == 12) && ...
У нас (guards.c:103) условие про тень нужно сверить: на уровне 12 тень
ОБЯЗАНА быть видимой (иначе Кид не достанет меч и бой не начнётся).
1.6 Меч исчезает (sword_disappears, seg002:0536)
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) — портала действительно
нет, уровень кончается фактом попадания в комнату:
// 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. Отдельного ИИ у него нет:
void autocontrol_Jaffar() { autocontrol_guard(); } // seg002:0697
То есть бой с Джафаром — обычный бой стражи, отличаются только спрайты, HP и три спецсобытия ниже. Это хорошая новость: боёвка у нас есть.
2.2 Встреча (meet_Jaffar, seg002:0544)
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), но без условия по уровню:
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)
} 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), срабатывающая при уходе Кида ВЛЕВО:
if (leveldoor_open == 2) { get_tile(24, 0, 0); trigger_button(0, 0, -1); }
То есть смерть Джафара не открывает дверь сама — она ставит флаг, а дверь
открывается кнопкой, «нажатой» при уходе влево. trigger_button у нас есть.
2.4 Падающие плиты (check_fall_flo, seg000:1319)
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, так что
код на виду.
2.5 Поза входа
tbl_entry_pose[13] = 2 — у нас в таблице уже есть; проверить, что режим 2
(seg003:172) реализован.
3. Уровень 14 — принцесса и конец игры
Боя нет вовсе (tbl_guard_type[14] = -1). Всё сводится к одному событию:
// 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 не имеет смысла раньше, чем на него можно попасть.
- ✅ Уровень 12 — тень:
init_shad_12,autocontrol_shadow_level12,united_with_shadow, общий урон, «убил тень — убил себя»,sword_disappears, появление плит. Сделано 2026-08-13, хост-наборSprPoP/tests-host/t_shadow.c(45 проверок); живой проверки в MAME ещё не было — доскаL12-SHADOW. - ✅ Переход 12 → 13 (§1.7) — room-триггер вместо двери и флаг
pop_seamless(не сбрасывать HP). Сделано там же. - ✅ Уровень 13 — Джафар:
guard_notice_timer,on_guard_killed/Jaffar_exit, три спецкейса loose (§2.4) иcheck_fall_flo. Плюс СПРАЙТЫ визиря (VIZIER.DAT) — без них он рисовался обычным стражем. Сделано 2026-08-13, хост-наборSprPoP/tests-host/t_jaffar.c(44 проверки); живой проверки в MAME ещё не было — доскаL13-JAFFAR. - Уровень 14 — довести до комнаты 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.