Files
Sprinter-SDCC/applications/PoP/roomtest/TASKS_OPEN.md
T
snark13 6673279cef PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
  poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
  curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
  УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
  стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
  только в кадрах 150..189.  Пока ветка была одна (charid_2_guard), скелет
  получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
  комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.

Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
  guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
  Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
  меняется вместе со стражем.  Грабля: gfx_pal_load отдаёт указатель в BIOS,
  а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.

Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
  Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
  после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
  обратно.  +48 байт W2.

Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
  seg008:1B06) и расширение перебора объединённым прямоугольником
  «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
  кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
  (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
  «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
  вернувшейся кучей костей.

Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.

Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).

Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).

В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:11:03 +03:00

36 KiB
Raw Blame History

roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-07)

Что берём в работу сейчас и в каком порядке. Каждая запись: что сделать, почему именно сейчас, чем подтверждать результат.

  • закрытые задачи с протоколами и замерами — TASKS_CLOSED.md;
  • открытые баги — bug_list.md, закрытые с разбором корней — bug_closed.md;
  • планы фаз — ../docs/PORT_PLAN.md, ../docs/layout_plan_v2.md, ../docs/levels_plan.md.

Состояние на 2026-08-07: пользователь прогнал ВСЕ комнаты уровней 1 и 2 — крупных багов нет. Приёмки L1-PASS и L2-PASS закрыты; с прогона открыты три записи по уровню 2 (цвет стража, брызги по стражу, чит +/ в бою) — все в bug_list.md. Уровень 3 играется, но приёмки не было: сначала чомперы и скелет.

Правило проекта в силе: механику сверять с ../SDLPoP/src/ ДО кодинга; диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не гипотезой (memory defer_unexplained_quirks).


ТЕКУЩАЯ ЦЕЛЬ: уровни 1–3 (подземелье) отлажены целиком

Решение 2026-08-04: palace (уровни 4+) откладываем, доводим до играбельности три dungeon-уровня. Основание — они не требуют ни одного нового ассета фона: инвентарь тайлов, снятый с res200N.bin, показывает, что новое появляется только так —

ур. 1  empty, floor, spike, pillar, gate, closer, potion, loose, debris,
       opener, level_door L/R, torch, wall, skeleton, sword     ← всё есть
ур. 2  bigpillar_bottom(8), bigpillar_top(9), doortop(12)       ← есть (2026-08-04)
ур. 3  chomper(18)                                              ← НЕТ механики
ур. 4  lattice_pillar(25)…lattice_right(29) + тайлсет palace    ← отложено
# Задача Что Блокирует
1 GUARD-PHYS физика стража = физика Кида (одна над Char) ядро сделано 2026-08-07; открыт живой сценарий в MAME
2 L3-CHOMP чомперы (5 шт) прохождение ур. 3
3 L3-SKEL скелет (единственный противник ур. 3) прохождение ур. 3
4 L3-PASS приёмка уровня 3 (обход комнат) закрытие цели
L1-SPEED игра на ~39 % быстрее оригинала ощущение от ВСЕХ уровней; берётся в любой момент
TUNE-1 параметры движка → cfg-файл (сейчас pop_tune.h) отладка таймингов и моды; берётся по мере надобности

Сделанное по этой цели — в TASKS_CLOSED.md: L2 (машинерия уровней), L2-PASS, L1-PASS, L3-CHKP (чекпойнт уровня 3), MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.


Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3

Сюда попадает то, что уже работает в проверочном прогоне, но должно быть подтверждено на сквозных прогонах уровней — потому что задевает механику шире, чем собственный сценарий.

  • Зацеп ПРЯМО В ПРЫЖКЕ (POP_ENABLE_JUMP_GRAB, pop_tune.h, сделан 2026-08-06, предварительно проверен пользователем). Почему нужен именно финальный прогон: точки вызова стоят не только в check_action, но и в ОБЕИХ ветках check_bumped — то есть код вклинивается перед обычным ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На уровнях 1–3 это надо специально потрогать в паре мест каждого уровня. Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — enable_jump_grab), так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при включённых enhancements. Прогон 2026-08-07 (уровни 1 и 2) регрессий рядом не показал, но специально на удар о стену с Shift не проверялся.

P0 — делаем сейчас

GUARD-PHYS. Страж живёт по тем же правилам, что Кид — ЯДРО СДЕЛАНО 2026-08-07

Что уже работает (решение пользователя: переносим физику на Char, без предварительных замеров — иначе третий-четвёртый экземпляр той же логики неизбежен).

  • физика переведена на Char: pop_map.c целиком работает с активным персонажем, а кто в Char — решают окна loadkid/savekid и loadshad/saveshad, как в оригинале (seg006:809). Два входа: pop_phys_tick (порт хвоста play_kid_frame) и pop_guard_phys_tick (порт play_guard_frame) — списки вызовов отличаются ровно тем, чем в оригинале;
  • take_hp стал общим (pop_take_hp в резиденте pop_guard.c): урон идёт тому, кто в Char, по charid. Раньше у боёвки и у физики были свои копии, причём у физики неверная — правила hitp_curr мимо дельты и про не-Кидов не знала;
  • порт веток по charid в land() (seg005:173): страж гибнет с двух рядов, тень падает как с одного, у не-Кида приземление даёт боевую стойку; check_guard_bumped (seg004:0522), droppedout + guard_follows_kid_down (seg002:09F8), check_guard_fallout (seg002:0241);
  • pop_savekid_state снова полное Kid = Char, а pop_load_fram_det_col пересчитывает колонку ЛЮБОМУ персонажу — обе заплатки существовали только потому, что физика знала один Kid.

Проверено: tests-host — трассы Кида не изменились ([phys] ok: 1723, тот же эталон), новый набор t_char (32 проверки) покрывает ветки по персонажам; в MAME проверено, что игра жива (респавн, бег, падение, приземление). Цена: _CODE +80 Б, банк 3 (pop_map) 7973 → 8485 (51.8 %), банк 1 (guards) 2042 → 2156.

Проверено пользователем 2026-08-07: страж СПРЫГИВАЕТ ЗА КИДОМ на ряд ниже — связка «ИИ + физика» работает вживую, не только в тестах.

follow_guard портирован и проверен в MAME (2026-08-07): уровень 1, бой в комнате 3, Кид отступает влево — страж приходит следом (Guard.room 3 → 2, X перенесён через шов), ровно как в SDLPoP. Условия отбора покрыты тестами t_char (7 сценариев: пороги 91/165, «не бой», мёртвый, вверх/вниз, занятая соседняя комната). Сцена вскрыла отдельный баг — BUG-SWORD-GHOST-1: при переходе в бою Кид прячет меч и дальше дерётся пустой рукой.

Осталось (потому и запись открыта):

  1. check_chomped_guard — вместе с L3-CHOMP;
  2. ветки check_guard_fallout для тени и скелета (скелет возрождается в комнате 3) — вместе с L3-SKEL;
  3. страж, нажимающий напольную кнопку, вживую не проверялся (код — общий check_press).

Почему это была ОДНА физика, а не «сделаем стражу свою». В оригинале слот Guard — не «стражи», а все НЕ-Киды: тем же play_guard_frame ходят страж (charid 2), скелет (4, ур. 3), тень (1, ур. 4/5/6/12), визирь-Джаффар (ур. 13), толстяк (FAT, ур. 12) и мышь (0x18, ур. 8) — tbl_guard_type = {0,0,0,2,0,0,1,0,0,0,0,0,4,3,1,1}, а autocontrol_opponent (seg002:628) разводит их ТОЛЬКО по ИИ. То есть второй экземпляр логики пришлось бы делать не один раз, а пять.

Гард по X: страж не уходит САМ — но его МОГУТ ПЕРЕНЕСТИ. Поправка к формулировке, которая была здесь раньше («страж не покидает комнату ни в каком виде») — она неверна, контрпример дал пользователь: в SDLPoP страж из комнаты 3 оказывается в комнате 2 вслед за отступающим Кидом.

Разделять надо два разных механизма:

  • своим ходом — не может. Физика персонажа слота Guard обёрнута Char.room == drawn_room и Char.x >= 44 && Char.x < 211 (seg000:1252), и никакого check_leave в его списке вызовов нет. Провалившегося ниже комнаты убирает check_guard_fallout (seg002:0241) — вниз он не уходит.

  • следом за Кидом — переносит движок. exit_room (seg002:03C7) вызывается ПОСЛЕ того, как комнату сменил Кид, и решает судьбу стража:

    if (Guard.alive < 0 && Guard.sword == sword_2_drawn) {      // жив и В БОЮ
        if (guards_tile[kid_room1] >= 30 ||                    // в новой комнате
            guards_seq_hi[kid_room1] != 0) {                   // своего живого нет
            if (ушёл ВЛЕВО)  { if (Guard.x >= 91)  leave = 1; } // страж далеко — остаётся
            else if (ВПРАВО) { if (Guard.x < 165)  leave = 1; }
            else if (ВВЕРХ)  { if (Guard.curr_row >= 0) leave = 1; }  // всегда → не идёт
            else             { if (Guard.curr_row < 3)  leave = 1; }  // вниз → не идёт
        } else leave = 1;
    } else leave = 1;
    leave ? leave_guard() : follow_guard();
    

    follow_guard (seg002:039E) стирает guards_tile в ОБЕИХ комнатах (0xFF — «стража здесь нет», чтобы он не раздвоился) и гонит стража через goto_other_room в окне loadshad/saveshad.

Что это значит для нас. У нас в enter_room (roomtest.c:265) стоит безусловный pop_guard_leave() — то есть всегда ветка leave_guard, и страж ВСЕГДА остаётся. Портировать надо сам exit_room-выбор: условия «жив + меч вынут + в целевой комнате нет своего стража + он у нужного края» и follow_guard. Пороги 91/165 — это «страж у того края, в который ушёл Кид»; вверх и вниз оригинал не пускает никогда.

Чем подтверждать: уровень 1, комната 3 — начать бой, отступить влево в комнату 2: страж обязан прийти следом и продолжить бой (как в SDLPoP на скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ живой страж — переход не происходит.

L3-CHOMP. Чомперы — механика уровня 3

Где: 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти на каждом уровне, так что это вложение не только в ур. 3.

Что портировать: animate_chomper (seg007) — состояние в room_modif, как у пик/ворот, значит шаблон уже отработан (gates_spikes_plan.md); коллизия и смерть Кида в сомкнутых челюстях (seg004/seg006); отрисовка кадра по модификатору (draw_tile_anim, ветка tiles_18_chomper).

Риск: банк 2 (pop_bg) занят на 90.8 %, свободно 1503 Б (замер 2026-08-07, было 2213 Б) — считать место ДО кодинга (levels_plan.md §5.1), иначе повторится «банк 2 упёрся в потолок» (коммит 2f3e854). Свободные номера банков есть (5+).

Ассеты УЖЕ упакованы (2026-08-07): pop_pack_bg.py кладёт в атлас весь набор кадров чомпера явно (CHOMPER_BOT_IDS 101-105, CHOMPER_TOP_IDS 111-113, CHOMPER_FORE_IDS 106-110 + кровь 114-123 mono-силуэтом цветом 12). Раньше в атласе не было НИ ОДНОГО его кадра: render_room.py пропускает анимированные тайлы, а tile_table[0x12].base_id = 0 — отсюда и «чомпера не видно вовсе». Рост: pop_env3.atl 5200 -> 12937 Б, pop_fore.atl 7763 -> 12359 Б, число EMM-страниц НЕ изменилось (7).

Чем подтверждать: комната 22 уровня 3 — чомпер (2,6) анимируется и рисуется (сейчас его не видно вовсе, см. раздел «Уровень 3» в bug_list.md); смерть Кида в сомкнутых челюстях.

L3-SKEL. Скелет — единственный противник уровня 3

Важно: в данных уровня 3 стражей нет вообще (guards_tile пуст во всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных», а спецсобытие check_skel (seg002:1044):

в комнате 1, когда Kid.curr_col == 2 или 3 и дверь уровня открыта, тайл tiles_21_skeleton (комната 1, тайлпос 15) стирается в пол, а на его месте поднимается персонаж: charid_4_skeleton, меч сразу вынут, seq_88_skel_wake_up, skill 2, HP 3.

Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают.

Сделано 2026-08-07 (ждёт живой проверки на уровне 3):

  • атлас скелетаpop_pack_guard.py SKEL собирает poc/res/skel/g0..g3.atl (28 кадров, 30 КБ, 4 EMM-страницы); упаковщик получил параметр набора (GUARD/SKEL). Палитра у скелета СВОЯ (SKEL/res750.pal), а не из res10.bin: на уровне 3 tbl_guard_type != 0, значит curr_guard_color = 0 и оригинал палитру не подменяет вовсе;
  • pop_guard_load выбирает набор по типу уровня и заливает палитру скелета; Makefile кладёт атласы в SKEL\ на диск;
  • check_skel (seg002:1042) — порт в guards.c: уровень 3, комната 1, дверь уровня открыта, Kid.curr_col 2 или 3, тайл 21 на (5,1) → тайл стирается в пол (обе страницы), персонаж встаёт с seq_88_skel_wake_up, мечом наголо, skill 2, HP 3. Зовётся из главного цикла ДО логики персонажей, как в play_frame;
  • leveldoor_open — флаг появился (pop_state.c), взводит анимация двери при modif >= 43 (seg007:456);
  • enter_guard — ветка charid_4_skeleton: встаёт сразу активным (меч вынут), а не в стойке покоя;
  • возрождениеcheck_guard_fallout: упавший скелет, под комнатой которого лежит комната 3, появляется там снова (x 133, ряд 1);
  • ИИautocontrol_skeleton (seg002:685): меч у скелета вынут всегда.

Регресс: tests-host зелёные, t_char вырос до 65 проверок (добавлены два сценария возрождения). Цена: _CODE +80 Б, банк 1 (guards) 2342 → 2519, банк 4 (gdraw) 3875 → 4269.

Не проверено вживую: сцена требует пройти уровень 3 до кнопки, которая открывает выход — без открытой двери скелет по условию не встаёт.

Что нужно:

  • charid_4_skeleton в enter_guard/pop_guard_enter (seg002:196): при tbl_guard_type[level] == 2 персонаж поднимается с вынутым мечом и последовательностью seq_63_guard_active_after_fall, а не seq_77_guard_stand_inactive;
  • скелет бессмертен — чит K его уже не берёт (pop_guard_kill проверяет CHARID_4_SKELETON), но и боёвка должна возвращать его к жизни (seg002:252);
  • новый атлас: ../SDLPoP/data/SKEL/ (29 файлов) — pop_pack_guard.py сейчас прибит к GUARD/, нужен параметр набора (tbl_guard_dat = GUARD/FAT/SKEL/VIZIER/SHADOW). Здесь же удобно закрыть BUG-GUARD-COLOR-1: палитра стража подменяется по guards_color, и оба изменения живут в одном упаковщике.

L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный)

Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA). В оригинальной VGA-версии кладка уровня 3 ЗЕЛЁНАЯ, а уровней 1-2 — серо-синяя. В SDLPoP все подземелья одинаковые, поэтому по нему разницу не увидеть.

Почему в SDLPoP её нет — проверено, не гипотеза. Механизм там ЕСТЬ (seg000:1140, «Level colors (1.3)»):

int level_color = custom->tbl_level_color[current_level];
if (level_color != 0) {
    byte* env_pal  = level_var_palettes + 0x30*(level_color-1);
    byte* wall_pal = env_pal + 0x30 * custom->tbl_level_type[current_level];
    set_pal_arr(0x50, 0x10, (rgb_type*)env_pal);    /* chtab_6 environment */
    set_pal_arr(0x60, 0x10, (rgb_type*)wall_pal);   /* chtab_7 wall        */
}

tbl_level_color (data.h:842) = {0,0,0,1,0,0,0,1,2,2,0,0,3,3,4,0}у уровня 3 цвет 1, у 7 тоже 1, у 8/9 — 2, у 12/13 — 3, у 14 — 4. Но level_var_palettes — это ресурс 20 из PRINCE.DAT (только версии 1.3/1.4), а в SDLPoP/data/PRINCE/ его НЕТ: там лежит лишь res10.bin (палитры стражей). Значит level_var_palettes == NULL и вся ветка молча пропускается — отсюда одинаковые подземелья.

Данные у нас есть. В ../MSDOS/PRINCE.DAT ресурс 20 присутствует: offset 22790, 240 байт = 5 палитр × 16 цветов × 3 байта (6-битные каналы, как res10).

Что делать (когда дойдём до вида уровня 3).

  1. Достать ресурс 20 из MSDOS/PRINCE.DAT (упаковщику придётся читать сам .DAT — сейчас все скрипты берут распакованные PNG из SDLPoP);
  2. сгенерировать таблицу палитр рядом с pop_guard_pal.h;
  3. при загрузке уровня заливать слоты 0x50..0x5F (env) и 0x60..0x6F (wall) — у нас ровно эти базы (pop_pack_bg.load_indexed: pal_base = 0x60 для WALL, 0x50 для env), то есть совпадение со set_pal_arr один в один;
  4. wall_pal = env_pal + 0x30 * tbl_level_type[level] — для подземелья (level_type == 0) обе палитры одинаковые.

Грабли, уже пойманные на цвете стражей: gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08), а BIOS читает только #4000-#BFFF — таблицу нельзя передавать прямо из банка (0xC000+), надо копировать в W1/W2 (см. BUG-GUARD-COLOR-1).

DRAW-CHAR. Отрисовка — ОДНА на всех Char, как физика после GUARD-PHYS

Почему заведено (пользователь, 2026-08-07). После GUARD-PHYS физика стала общей над Char, а ОТРИСОВКА так и осталась продублированной: kid_draw и pop_guard_draw считают load_frame_to_obj, флип и heal-прямоугольники каждая по-своему, окклюзия — двумя проходами (pop_fore_over_kid / pop_fore_over_char). Общее вынесено лишь частично (char_footprint, other_overlay_tile, pop_sword_draw), а СПИСКИ ВЫЗОВОВ разные.

В оригинале она общая — проверено. seg008 держит два входа с ИДЕНТИЧНЫМ телом:

add_kid_to_objtable (seg008:22F0)      add_guard_to_objtable (seg008:2324)
  loadkid()                              loadshad()
  load_fram_det_col()                    load_fram_det_col()
  load_frame_to_obj()                    load_frame_to_obj()
  stuck_lower()                          stuck_lower()
  set_char_collision()                   set_char_collision()
  set_objtile_at_char()                  set_objtile_at_char()
  redraw_at_char()                       redraw_at_char()
  redraw_at_char2()                      redraw_at_char2()
  clip_char()                            clip_char()
  add_objtable(0)                        add_objtable(1 тень / 2 страж)

Различаются ТОЛЬКО окно (Kid/Guard), тип объекта и спецкейс тени у зеркала (уровень 4). То есть та же схема, что play_kid_frame / play_guard_frame у физики.

Цена дублирования уже видна. Скелет, падающий в пропасть (ур. 3), рисовался ПОВЕРХ верхней грани пола соседней колонки: проход other_overlay_tile (порядок midtable — «тайлы позже персонажа ложатся поверх него») вызывался только у Кида. Симптом закрыт 2026-08-07 переносом прохода в pop_fore_over_char, но корень — именно дублирование. Раньше это не всплывало потому, что ОБЫЧНЫЙ страж в пропасть не падает: его физика гейтится x ∈ [44,211), а провалившегося убирает check_guard_fallout. Тем же путём пойдут тень (ур. 4/5/6/12), визирь (13), толстяк (12) — расхождение будет повторяться на каждом.

Что сделать: свести к одному набору функций над Char, оставив параметрами ровно то, что различается в оригинале:

  • окно (loadkid/loadshad — уже есть);
  • набор атласов и таблица кадров (kid* / GUARD / SKEL — уже выбирается в pop_guard_load);
  • слот heal-прямоугольников (у каждого персонажа свой, по страницам дабл-буфера);
  • тип объекта для порядка (Kid / тень / страж).

Не портированы вовсе и относятся сюда же: stuck_lower, redraw_at_char2, clip_char (см. memory pop_clip_char_todo).

Чем подтверждать: падение скелета в пропасть с разных X (сегодняшний сценарий), бой стража на ур. 1-2 без регрессий, tests-host зелёные.

L3-PASS. Приёмка уровня 3 — обход всех комнат

Как L1-PASS/L2-PASS: сквозной проход руками плюс обход комнат читом ROOMNAV. Осмысленна ТОЛЬКО после L3-CHOMP и L3-SKEL — без них уровень заведомо неполон, и половина наблюдений будет «механики нет».

Что уже снято и пригодится (справка ниже): комнаты 23 и 24 недостижимы и полностью пусты — баги в них не в приоритете; чекпойнт уровня 3 сделан (L3-CHKP), его тоже надо потрогать вживую: уйти влево из комнаты 7, умереть, проверить респавн в комнате 2 и снятую loose-плиту (7, кол 4, ряд 0).

Справка: связность комнат уровней 1–3 (снято 2026-08-05)

Обход графа roomlinks (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись 13/18/24 (см. «НЕ БАГИ» в bug_closed.md). Скрипт разовый, в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии.

уровень старт недостижимы признак
1 к.1 (0,0) 13, 18, 24 ссылки наружу есть, обратных нет
2 к.5 (1,3) нет граф полностью симметричен, все 24 достижимы
3 к.9 (2,4) 23, 24 то же, что на 1: односторонние ссылки, обе комнаты полностью пустые
ур.3:  23 L→4,  у 4  R=22   |  24 L→2,  у 2  R=7
       23 R→22, у 22 L=4    |  24 R→7,  у 7  L=2
                            |  24 U→16, у 16 D=0
       на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty

То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот же: в 23/24 возможен «мусор в шве» (наш рендер кромки читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов там низкий, в игре их не видно.

Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода (единственная связь — вверх, откуда Кид падает):

к.10  U→4    пики(2,2) + пол — падение из комнаты 4
к.14  U→21   шахта 2 тайла шириной, дно = обломки
к.17  U→15   то же

Это не баги данных: в 14/17 попадают только падением насмерть, а из 10 (если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать «застрял» багом.


P1 — берётся в любой момент

L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)

Сверка таймингов: оригинал — BASE_FPS = 60 при base_speed = 5 тиков на логический кадр (SDLPoP/src/types.h:1373, data.h:869) = 83.3 мс, в бою fight_speed = 6 = 100 мс. У нас roomtest.c ждёт три gfx_wait_vsync() = 60 мс, и отдельной скорости боя нет — то есть примерно +39 % к скорости эталона. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою. Условие «делать ПОСЛЕ CLIP-1» снято — CLIP-1 закрыт. Проверка — секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз».

TUNE-1. Параметры движка — в конфиг, а не в код

Что уже есть. pop_tune.h — все настраиваемые числа собраны в одном заголовке: чекпойнт уровня 3 (POP_CHKP_*), отладочное окно решётки (POP_DBG_GATE_HOLD), включатель зацепа в прыжке (POP_ENABLE_JUMP_GRAB). Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а не константой по месту.

Что нужно сделать. Читать их из ФАЙЛА рядом с exe, чтобы менять без пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под моды. Формат: простой ini/ключ=значение, парсер на ~50 строк (числа, комментарии ;, неизвестные ключи игнорировать), файл необязателен — нет файла, значит зашитые дефолты. Секции по смыслу: [level], [debug], [enhancements].

Ориентир — SDLPoP. У него это custom_options_type (types.h) + SDLPoP.ini + меню Settings/Mods; наши имена намеренно совпадают с его (custom->имя), чтобы сверка оставалась механической. Осмотр его меню и опций — часть задачи: у него уже разложены по группам стартовые HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало, чекпойнт), тайминги ворот и пик, скорости, а отдельной группой — fixes/enhancements (включая enable_jump_grab, который мы уже портировали). Брать всё подряд не надо: переносим по мере того, как константа реально понадобилась в игре.

Оговорка по памяти. Парсер и таблица параметров — холодный код, исполняется один раз при старте: кандидат в банк, а не в резидент W1.

MEM. Следующий шаг разгрузки W1/W2

pop_ctrl.c уехал в банк 5 (MEM-BANK5, куча 180 Б → 2298 Б; после снижения --max-allocs — 2751 Б). Следующий кандидат по тому же критерию (не размер, а частота вызова и отсутствие горячих банк→банк переходов) — расщепление pop_kid.c: холодная половина (загрузка страниц спрайтов, pop_kid_load) в банк, движок кадров (load_frame/play_seq, 2×/кадр) оставить в резиденте.

Брать по факту нехватки места, не заранее. Таблица резидентного кода по модулям и разбор, почему pop_level.c в банк НЕЛЬЗЯ, — в TASKS_CLOSED.md.


Отложено осознанно (не брать, пока не появится причина)

  • KBD-1, остаток — «иногда при зажатом Shift стрелка всё-таки пропускается» (ощущение пользователя, не замер: счётчики на 35 нажатиях подряд потерь не показывают). Решение пользователя 2026-08-01: отложено до финальной полировки. Где именно осталась дыра и что делать, если вернёмся, — в TASKS_CLOSED.md (там же весь протокол замеров и список того, что делать НЕЛЬЗЯ).
  • Quickload (Shift+F9) и остальные читы SDLPoP — оценка сделана (DBG-CHEATS), код не написан. Самое ценное и самое дорогое: сериализация Char + room_modif всех комнат + trob'ов + стражей (levels_plan.md §4), зато даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки позы.
  • Звук (CBL-эффекты, Фаза 5 PORT_PLAN.md) — геймплей не блокирует.
  • Таймер уровня / HUD времени / меню / сохранения — Фаза 6.
  • T-1 (пики: перерисовка по причине) — отдаётся почти бесплатно после T-2, отдельно не окупается.
  • Отключение мыши на время игры и замена PRNG../docs/ideas_backlog.md (оба дают доли процента кадра).
  • OPT-1 (хирургический редрой шва) — решено НЕ делать, стоимость транзиентная; разбор в bug_closed.md.