44 KiB
roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-08)
Что берём в работу сейчас и в каком порядке. Каждая запись: что сделать, почему именно сейчас, чем подтверждать результат.
- закрытые задачи с протоколами и замерами —
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_closed.md); открытым остался чит +/− в бою.
Уровень 3: скелет сделан и проверен в MAME 2026-08-07 (бой, падение в
пропасть, окклюзия), чомперов ещё нет.
Разгрузка банка 2 сделана 2026-08-08 (MEM-BANK2): 90.4 % →
35.8 %, свободно 10 512 Б — холодная половина уехала в банк 7
(pop_room.c), чомперам места с запасом.
DRAW-CHAR сделана и ПРОВЕРЕНА 2026-08-08 (протокол и замеры —
TASKS_CLOSED.md): отрисовка теперь одна на
всех Char. Банку 2 она дала всего +265 Б — место под чомперов дала уже
MEM-BANK2.
Правило проекта в силе: механику сверять с ../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 |
| — | DRAW-CHAR | отрисовка одна на всех Char — закрыта и проверена 2026-08-08 |
— |
| — | MEM-BANK2 | разгрузка банка 2 — закрыта 2026-08-08: 90.4 % → 35.8 %, свободно 10 512 Б | — |
| 2 | L3-CHOMP | СЛЕДУЮЩАЯ: чомперы (5 шт) | прохождение ур. 3 |
| — | L3-SKEL | скелет ур. 3 — сделан 2026-08-07, ждёт финальной приёмки | — |
| 3 | L3-PASS | приёмка уровня 3 (обход комнат) | закрытие цели |
| — | DRAW-COST | шаг 1 сделан (пропуск неизменившегося персонажа): в покое 210 % -> 116 %. ШАГ 2 — цена ОДНОЙ перерисовки: бегущий Кид даёт циан почти 100 % | плавность на ВСЕХ уровнях |
| — | 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.room3 → 2, X перенесён через шов), ровно как в SDLPoP. Условия отбора покрыты тестамиt_char(7 сценариев: пороги 91/165, «не бой», мёртвый, вверх/вниз, занятая соседняя комната). Сцена вскрыла отдельный баг — BUG-SWORD-GHOST-1: при переходе в бою Кид прячет меч и дальше дерётся пустой рукой.Осталось (потому и запись открыта):
Почему это была ОДНА физика, а не «сделаем стражу свою». В оригинале
слот 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_room−1] >= 30 || // в новой комнате guards_seq_hi[kid_room−1] != 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 расчищено (MEM-BANK2, 2026-08-08):
35.8 %, свободно 10 512 Б (было 1074 Б) — холодная половина слоя фона
уехала в банк 7 (pop_room.c). Считать место ДО кодинга всё равно
обязательно (levels_plan.md §5.1); чомпер — это анимация тайла (холодная
перерисовка), поэтому его отрисовка ложится в pop_room.c, а состояние —
в room_modif, как у пик и ворот.
Ассеты УЖЕ упакованы (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: на уровне 3tbl_guard_type != 0, значитcurr_guard_color = 0и оригинал палитру не подменяет вовсе;pop_guard_loadвыбирает набор по типу уровня и заливает палитру скелета; Makefile кладёт атласы вSKEL\на диск;check_skel(seg002:1042) — порт вguards.c: уровень 3, комната 1, дверь уровня открыта,Kid.curr_col2 или 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).
- Достать ресурс 20 из
MSDOS/PRINCE.DAT(упаковщику придётся читать сам.DAT— сейчас все скрипты берут распакованные PNG из SDLPoP); - сгенерировать таблицу палитр рядом с
pop_guard_pal.h; - при загрузке уровня заливать слоты 0x50..0x5F (env) и 0x60..0x6F
(wall) — у нас ровно эти базы (
pop_pack_bg.load_indexed:pal_base = 0x60для WALL,0x50для env), то есть совпадение соset_pal_arrодин в один; 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-COST. Кадр НЕ УКЛАДЫВАЕТСЯ в бюджет — нужна оптимизация
ШАГ 1 СДЕЛАН 2026-08-08: пропуск неизменившегося персонажа. Комната 1.3, труп стража, Кид стоит: было 210 % кадрового периода, стало 116 % (500 772 такта при бюджете 430 000). Отрисовка перестала быть узким местом: персонажи в покое не рисуются ВООБЩЕ (ноль вызовов
pop_heal_fastза кадр), весь фон — ДВА блита факелов (44 136 тактов). Механизм и почему метка позиционная — в шапкеpop_cdraw.h.
Исходные замеры пользователя (полосы бордюра, до шага 1). Уровень 1, Кид СТОИТ — то есть НЕ худший случай, ни боя, ни движения:
| комната | синяя (ввод+heal+логика) | зелёная (фон) | циан (спрайты) | итого |
|---|---|---|---|---|
| 3, страж УБИТ | ~80 % | ~20 % | ~110 % | ~210 % |
| 2, стража НЕТ | ~60 % | ~20 % | ~60 % | ~140 % |
Разница ровно в теле убитого стража: +20 % синей и +50 % циана. Труп
сохраняет charid != 0, поэтому каждый кадр честно проходил весь путь
живого персонажа (heal → спрайт+clip_char+брызги+клинок → fore-проход), хотя
его кадр постоянен до выхода из комнаты.
Что сделано (шаг 1). Не спецкейс «мёртвый», а общее правило: у каждой
страницы дабл-буфера свой снимок ВХОДОВ отрисовки слота; совпал снимок,
спрайт этой страницы цел и фон в его прямоугольнике не трогали — heal, блит
и fore-проход пропускаются целиком. Покрывает и труп, и стоящего Кида, и
ждущего стража. Детали контракта — pop_cdraw.h, реализация —
pop_char_skip_mask / cd_quiet в pop_cdraw.c, метка фона —
pop_cd_touch в резидентном pop_tile.c.
Грабли, на которые наступили по дороге: сначала метка была ФЛАГОМ «фон трогали хоть где-то» — и выигрыш оказался ровно нулевым, потому что факелы анимируются каждый кадр и гасили пропуск для всех персонажей сразу (замер: 597 684 такта, как без оптимизации). Метка обязана быть ПОЗИЦИОННОЙ.
Остаточный эффект от объединения прямоугольников. Метка одна на страницу — объединение всех правок фона. В комнате 1 два факела дают прямоугольник x 40…88, а стоящий там Кид занимает x 32…44: пересечение 4 px, и он перерисовывается каждый кадр (140 % против 94 %, если отойти). Если понадобится — держать не один прямоугольник, а 3-4 отдельных и мержить только при переполнении.
Шаг 2 (СЛЕДУЮЩИЙ, назначен пользователем): ДВИЖУЩИЙСЯ Кид
Пропуск закрывает только покой. Наблюдение пользователя 2026-08-08: как только Кид побежал, циан-полоса (спрайт + fore-проход) вырастает почти до 100 % кадрового периода — и это на ОДНОГО персонажа. То есть цена одной перерисовки персонажа сама по себе непозволительно велика, и её надо резать по существу, а не пропусками.
Куда смотреть (мерить каждое, метод — z80_profiling_method):
- разделить замером спрайт-блит и fore-проход: у fore уже была история
78 % кадра до кэша кладки (
pop_fore_layer_cost), он и сейчас главный подозреваемый; - fore-проход перебирает 2×2…3 тайла и в каждом разбирает кладку заново — кэш «в этом тайле переднего слоя нет вовсе» снял бы половину;
- расширять перебор ТОЛЬКО по накладным спрайтам (клинок/брызги), а не по всему окну клипа: габарит кадра уже покрыт футпринтом + правилом меча;
- heal + блит спрайта: сейчас это два прохода по одной площади; посмотреть, нельзя ли стирать только РАЗНОСТЬ прямоугольников при мелком сдвиге.
Куда идти дальше в ПОКОЕ — там это ЛОГИКА, а не отрисовка. Разбивка работы кадра брейкпоинтами (комната 3, труп, Кид стоит; всего 500 772 такта):
| участок | тактов | % кадра |
|---|---|---|
ввод + check_skel + луч видимости + ctrl_tick + heal |
65 862 | 15 % |
kid_tick + физика + страж + боёвка |
260 022 | 60 % |
loose_tick + process_trobs |
119 562 | 28 % |
redraw_needed + шов + спрайты + метка |
55 152 | 13 % |
| — из них два блита факелов | 44 136 | 10 % |
- 60 % на тик персонажей при том, что оба СТОЯТ — первый кандидат.
Смотреть
pop_phys_tick/pop_guard_phys_tick(банк 3) иpop_guard_tick(банк 1): сколько там работы, которую неподвижный персонаж делать не обязан, и сколько стоит трамплин на каждом шаге. - 28 % на
loose_tick+process_trobsв комнате БЕЗ единой ловушки — явно перебор; разобрать, что там сканируется каждый кадр (список trob,pop_trob_modifсоседа для шва,pop_room_link). - Запечь труп в фон (как щебень: тело в фоне + fore-часть поверх Кида) — тогда бесплатным станет и проход Кида ПО телу, который сейчас снимает пропуск по правилу «слоты рядом». Предложено пользователем 2026-08-08.
Мерить в ЭТОЙ точке (комната 3, страж убит, Кид стоит) — она воспроизводима
и даёт нижнюю границу; худший случай (бой + бег + падающая плита) считать
отдельно. Метод — memory z80_profiling_method (брейкпоинты с
totalcycles); полосы бордюра показывают только одну картинку из периода и
годятся лишь для раскладки по фазам.
Наблюдение пользователя на приёмке DRAW-CHAR 2026-08-08: циан-полоса профиля (спрайты + fore) выросла — тогда «в пределах».
Отчего именно. Раньше перебор тайлов у Кида шёл строго по футпринту
кадра (char_x_left/right, seg006:1021), а он УЖЕ спрайта; расширение
перебора окном спрайта (клинок и брызги уходят за габарит кадра) было
только у соперника. DRAW-CHAR сделала его общим — то есть у Кида теперь на
колонку-другую больше fore_tile за кадр. Это не регрессия «лишней
работы», а недостающая ранее корректность: тайл, который спрайт задевает,
обязан вернуть свой передний слой поверх него.
Если fore-проход снова станет узким местом (сейчас он в покое не выполняется вовсе): расширять перебор ТОЛЬКО по накладным спрайтам (клинок/брызги), а не по всему окну; кэш «в этом тайле fore-слоя нет вовсе»; считать окно клипа в тайловых координатах один раз.
DRAW-CHAR — ЗАКРЫТА И ПРОВЕРЕНА 2026-08-08
Отрисовка сведена к одному набору функций над Char (pop_cdraw.c), проход
окклюзии — один на всех (pop_fore_over_char). Разбор, таблица «было →
стало», пять починенных расхождений и замеры — в
TASKS_CLOSED.md. Оттуда же список того, что
надо потрогать вживую при приёмке уровней.
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.