Commit Graph

71 Commits

Author SHA1 Message Date
snark13 52bcc65a62 Особенность: убитый за правым краем страж не виден ни в одной комнате
Вопрос пользователя после боя в комнате 15: Кид вытеснил стража вправо
(из-за края торчал только меч), убил — труп не появился ни в 15, ни в
соседней справа.

Это не наш баг, а сложение трёх механизмов оригинала: физика стража
работает только в полосе x 44..211, поэтому комнату он не менял;
мёртвый за Кидом не идёт (follow_guard требует alive < 0), и leave_guard
сохраняет его в прежнюю комнату; а из чужой комнаты страж не рисуется
вовсе — при Guard.room != drawn_room оригинал гасит слот (seg000:422).

Труп остаётся приписан комнате 15 с guards_x за правым краем: при
возврате восстанавливается там же, то есть вне видимого поля.

Живьём в SDLPoP сценарий не воспроизводился — вывод из чтения кода, о чём
в записи сказано прямо.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:23:41 +03:00
snark13 41deb69013 Реестр: P15 закрыт замерами обеих позиций
Лёгкая: 628 542 -> 464 796 (-163 746), персонажи не рисуются вовсе.
Тяжёлая: 758 358 -> 617 487 (-140 871), рисуется только Кид — он
действительно стоит под пламенем, а страж нет.  Пятирастровые кадры в
тяжёлой позиции исчезли (было 27 %).

Итог восьми позиций: 801 768 -> 464 796 в лёгкой (-42 %).  До цели
430 000 осталось 35 000 в лёгкой и 187 000 в тяжёлой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:09:47 +03:00
snark13 17c41b32de P15 переписан: перекрытия нет, виновата грубость метки
Пользователь поймал ошибку в моём расчёте, глядя на экран: страж целиком
правее пламени, пересекаться может только меч.

Проверка по памяти машины подтвердила и уточнила:

  страж, спрайт   x 257..284  y 18..56
  страж, клинок   x 241..261  y 31..37
  пламя факела    x 232..247  y  5..22

Ошибок было две.  Первая: координаты пламени я взял по предположению
«факел в колонке 7», а он в колонке 6 (пламя рисуется в ячейке правого
соседа).  Вторая, содержательная: ФИЗИЧЕСКОГО ПЕРЕКРЫТИЯ НЕТ ВООБЩЕ — по x
клинок и пламя пересекаются, но по y между ними девять пикселей зазора.

Настоящая причина: pop_cd_touch хранит метку как маску КОЛОНОК по 32 px на
ТРИ ряда по 63 px (cd_row_of).  Пламя (y 5..22) и клинок (y 31..37)
попадают в один ряд 0 и одну колонку 7 — cd_quiet считает слот задетым.
148 302 такта, 23 % кадра, за ложную тревогу.

Решение стало проще и точнее: хранить на колонку диапазон y вместо номера
ряда (10 x 2 байта x 2 страницы = 40 байт).  Расчётом проверено, что это
спасает стража и НЕ спасает Кида в тяжёлой позиции — там перекрытие
настоящее, и он честно перерисовывается.  Вариант с 8-пиксельными полосами
тоже работает, 16-пиксельные уже нет.

Прежние предложения (частичная перерисовка по пересечению, обрезка фона под
персонажем) записаны как НЕ НУЖНЫЕ: они решали задачу, которой нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:40:30 +03:00
snark13 a65da96960 CHAR-PARTIAL-REDRAW: обязательная задача о неподвижном персонаже
Постановка пользователя: проверять, нужна ли отрисовка стража, когда он
не двигается.  Если движется — лишние ~150 000 тактов приемлемы: в
оригинале во время боя число физических кадров на логический тоже растёт
на единицу.

Замер чтением pop_cd из памяти машины показал, насколько цена
несоразмерна поводу:

  страж       x 257..284, y 18..56   28 x 39
  пламя (0,7) x 264..279, y  5..22   16 x 18
  пересечение x 264..279, y 18..22   16 x 5

То есть пламя задевает страже только макушку — 80 пикселей, — а
перерисовывается он целиком за 148 302 такта (85 524 спрайт с клинком и
снимком + 62 778 fore-проход), это 23 % работы кадра.  Пересечение при
этом настоящее: дело не в грубости маски меток, проверено числами.

В задаче записаны два варианта: A — частичная перерисовка только
пересечения (безопаснее, укладывается в контракт pop_cd), B — не рисовать
фон там, где он всё равно перекрыт неподвижным персонажем (дешевле, но
обрезанное пламя попадёт в ОЗУ-копию и heal вернёт дыру, когда персонаж
сдвинется).

Заодно уточнено, чем НЕ является P13 (вопрос пользователя): это не
перерисовка комнаты заново каждый кадр — такой вариант стоил бы порядка
3 000 000 тактов, семь растровых кадров, и оригинал так тоже не делает.
Разница в цене ПОСЕЩЕНИЯ тайла: у нас fore_tile сразу блитит, у оригинала
add_*table только кладёт запись, а рисует один draw_table в конце.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:35:54 +03:00
snark13 d3049693b8 Реестр: сводка и статусы обновлены; замер тяжёлой позиции
Пользователь передвинул Кида на один осторожный шаг вправо (x = 106
вместо 99, колонка та же) — его спрайт начал пересекаться с тайлом (0,3),
где одновременно чомпер и пламя факела.

  фаза      лёгкая    тяжёлая
  синяя    259 500    257 520
  зелёная  181 068    180 870
  циан     187 761    319 842   (+132 081)
  работа   628 542    758 358

Период кадра: 4 растра в 335 кадрах, 5 растров в 123 (27 %).  Это уже не
«стабильно медленно», а рывки.

Куда ушли 132 тысячи: pop_char_draw(KID) 204 -> 54 738 и fore-проход
Кида 4 356 -> 93 486.  То есть Кид из «пропущен» превращается в
полноценного персонажа за ~144 000 — столько же, сколько страж.

Отсюда новая позиция P14: fore-проход персонажа, 62 778 у стража и
~89 000 у Кида, вместе около 152 000 = 20 % работы кадра.  Это самая
дорогая единичная статья.  У Кида он дороже потому, что в его футпринте
лежит чомпер со своим передним слоем.

P3 переведён в «частично сбылось»: выигрыш держится только пока персонаж
не подошёл к анимированному тайлу, а в игре он подходит постоянно.

Итог семи закрытых позиций: 801 768 -> 628 542, то есть -22 %.  До цели
430 000 остаётся снять 199 000 в лёгкой позиции и 328 000 в тяжёлой, а
всё оставшееся в реестре даёт порядка 100 000.  Арифметика не сходится —
в реестр записаны три возможных решения (P13, осознанное расхождение с
оригиналом, принять 4 растра), выбор за пользователем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:30:04 +03:00
snark13 6faf81016a Замер цианной фазы: крупного лишнего в отрисовке персонажа нет
Циан 188 004 не двигался ни от P1, ни от P5, ни от P2b — разложил его
зондами.

Хорошая новость: Кид УЖЕ пропускается (204 такта на pop_char_draw), то
есть надежда P3 сбылась после P1 — метка от чомпера до него больше не
дотягивается.  Страж же перерисовывается каждый кадр честно: пламя
правого факела (0,7) рисуется в ячейке (0,8), где он стоит, и реально
накрывает ему голову (пламя занимает y 5..22, страж 12..62).

Отрисовка стража — 148 302:

  pop_char_fore (2 трамплина в банк 2 + обход тайлов)  62 778   42 %
  клинок (sword_draw + overlay_add + clip_add)         27 522   19 %
  блит спрайта + clip_char_right                       20 982   14 %
  загрузка кадра и геометрия                           12 696    9 %
  pop_clip_char_top (трамплин банк 4 -> банк 3)         8 658    6 %
  снимок прямоугольника + cd_clip_add                   7 890    5 %
  gfx_w0_unmap + cd_sig_make                            4 968    3 %
  вход + cd_heal                                        2 946    2 %

Единственная явно лишняя статья — трамплин clip_char_top, и снять его
непросто: функции нужны get_tile и таблицы деления из банка 3, перенос в
резидент вернёт тот же трамплин внутрь.  Остальное — работа, которую
персонаж действительно делает.

Зонды переставлены с уже закрытых замеров (loose_tick, физика) внутрь
pop_cdraw; оснастка снимается позицией P12, когда оптимизация закончится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:53:49 +03:00
snark13 068e21b56a Реестр: P2b закрыт; иерархия референсов и трамплины в цикле
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:37:34 +03:00
snark13 da17a48576 Реестр: P2a закрыт, следующий P2b
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:09:30 +03:00
snark13 de68eb5cec P1: чомпер перерисовывался неизменной позой — минус 110 802 такта
Позиция заводилась с НЕПОЛНЫМ диагнозом.  Я приписал 190 260 тактов
пометке от факела (пламя лежит в ячейке правого соседа, то есть поверх
чомпера, и запекается каждый кадр).  Правка по этому диагнозу не дала
ничего: 769 002 против 768 684.

Зонд pop_dbg_kind показал факт: все 312 перерисовок прогона — вид
POP_RD_CHOMP, полная, и ни одной от факела.  Собственная пометка чомпера
просто перебивала пометку соседа.

Настоящая причина нашлась сверкой с animate_chomper (seg007:0448).
Оригинал заканчивает её так:

    if ((curr_modifier & 0x7F) < 6) redraw_at_trob();

то есть перерисовывает чомпер только пока фаза меньше 6 — пять кадров из
пятнадцати.  Это не оптимизация оригинала, а следствие таблицы поз:
chomper_fram1 = {3,2,0,1,4,3,3}, и с фазы 5 до конца круга поза одна и та
же.  Мы метили тайл каждый кадр, пока trob жив, а живёт он всё время, пока
Кид в том же ряду — то есть платили полный draw_tile плюс heal 32x64 за
неизменную картинку в двух третях кадров.

Сделано:

  1. пометка только при фазе < 6; на фазе 5 — обе страницы дабл-буфера
     (она последняя рисуемая, её поза обязана лечь на обе; вторую страницу
     пометка догоняет в кадре фазы 6, где поза та же — CHOMP_FRAM1[6] == 3);
  2. новый вид POP_RD_CHOMP_ANIM -> pop_chomp_anim_draw: три блита графики
     чомпера поверх свежего пламени, без heal и без остальных слоёв — порт
     ветки redraw_frames_anim (seg008:0211), где оригинал делает ровно
     draw_tile_anim_topright / draw_tile_anim_right / draw_tile_anim и
     никакого wipe;
  3. приоритет полной перерисовки над anim в pop_set_redraw: у оригинала
     это два независимых счётчика и full побеждает, а у нас вид один на
     тайл, и без проверки исход решал бы порядок trob'ов в списке.

Обе половины работают — замер даёт 40 % полных перерисовок и 60 % лёгких.
Работа 768 684 -> 657 882 (медиана), зелёная 294 510 -> 183 420.  В 40 %
кадров цена прежняя: там поза реально меняется, это честная работа.

Циан не сдвинулся ни на такт, то есть надежда P3 (Кид перестанет будиться
каждый кадр) пока не оправдалась — метки продолжают его будить.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:30:01 +03:00
snark13 f81b30eb68 Замер P2 и P6: главные статьи — физика двух Char и луч видимости
Синяя фаза (286 518) разложена зондами на 12 участков, process_trobs
(89 784) — на три.

Гипотеза, с которой я входил в замер, ОТВЕРГНУТА.  Я ждал, что дорого
обходятся банковые трамплины на спецсобытиях уровней — по аналогии с
pop_clip_char_top, где трамплин ради одной проверки стоит 8 892.  На деле
три спецсобытия (skel, mouse, killed_shadow) вместе стоят 1 650: они
гейтятся внутри и на уровне 11 выходят сразу.

Настоящие статьи синей:

  физика двух Char        105 246  (61 266 Кид + 43 980 страж)
  heal двух Char           67 734
  луч видимости стража  до 37 032  (вместе с frame_timers)
  pop_ctrl_tick            18 648
  логика стража            19 932

Физика съедает 13,7 % работы кадра при том, что ОБА персонажа стоят и кадр
позы не меняется.  Цена измерена, причина нет — это отдельная позиция P2a.
Луч видимости считается каждый кадр, хотя никто не двигался: гейт по смене
позиции/комнаты — позиция P2b, ждём −30 000.  heal отдельной правки не
требует, он уйдёт вместе с P1/P3.

process_trobs: префетч кодов тайлов с маппингом окна 0 — 11 058, обход
самих trob'ов ~43 000 (pop_trob_modif зовётся банковым вызовом на КАЖДЫЙ
trob, хотя комната одна), два факела ~36 000.  Цена одного pop_pot_b
измерена отдельно: 17 346, и это единственная группа в распределении —
значит в кадре его зовут только факелы.  Пиксели пламени 16x18 — 1 716,
то есть 10 % цены.

Заодно посчитано, достижим ли период 3 растра: снять надо 338 000, а сумма
ВСЕХ известных позиций даёт 357 000, из которых 160 000 держатся на одной
(P1).  Цель достижима, но без запаса.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:05:58 +03:00
snark13 6d7c1c8b6b P5: гейты холостого хода в pop_loose_tick — минус 33 840 тактов на кадре
Замер 11/15 показал, что loose-механика берёт 28 872 такта в комнате, где
не анимируется ни одна плита и не летит ни один кусок.  Раскладка зондами
m9..m12: два цикла по тайлам 9 852, обход 14 слотов mob 12 090, поиск
куска над головой Кида 5 868 (там ещё и банковый трамплин).

Два гейта:

  loose_any (статик pop_map.c) — «идёт ли анимация плит».  Ставят пять мест
  записи ненулевой фазы: make_loose_fall, ветка потолка в check_press,
  do_knock для обоих рядов и восстановление фазы из room_modif при входе в
  комнату.  Снимает его сам цикл, по факту прохода, в котором не осталось
  ни одной живой фазы.

  pop_mob_busy (резидент pop_state.c) — «занят ли слот падающего куска»
  (active или дочистка clean).  Ставит mob_alloc, снимает обход по факту
  пустой таблицы.  В резиденте, а не в pop_room.c, потому что читает его
  pop_map из банка 3, а писучие статики банкового модуля наружу не видны.

Гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация»: у
лежащей плиты-потолка фаза нулевая, и крутить нечего (вопрос пользователя).
Асимметрия намеренная — ложная единица стоит одного холостого прохода,
ложный ноль стоит застывшей навсегда плиты, поэтому взвод стоит рядом с
КАЖДОЙ записью, а снятие только по факту пустого прохода.

Стало: 132 / 996 / 546, вся функция 28 872 -> 2 760.  На кадре работа
801 768 -> 767 928.  Ожидание по реестру было -28 000.

Покрытие: новый phys_loose_gate_survives_room_change на пятое место взвода
(фаза восстановлена входом в комнату) — единственное, которое не прогонял
ни один тест, и дающее самый тихий отказ.  Мутационная проверка: со снятым
взводом тест падает (фаза 3 вместо 4).

Заодно отладочный старт сразу в целевую комнату: make ROOM=15 POS=2
(дефолт), roomtest стартует в 11/15 с Кидом в (0,2).  kid_init ставит
x = x_bump[col] + TILE_SIZEX, а это левая граница СЛЕДУЮЩЕЙ колонки — с неё
физика относила Кида в тайл чомпера, и он погибал на старте (найдено
пользователем).  Сдвиг внутрь на 2: колонку определяет весовая точка кадра,
поэтому число снято замером, а не выведено геометрией.

План работ между сессиями — docs/perf_registry.md §4: очередь позиций со
статусами, текущий бюджет сцены, рецепт её воспроизведения и метод замера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:49:19 +03:00
snark13 fd570c7eb8 Замер сцены 11/15 и единый реестр оптимизаций
Новая целевая сцена: уровень 11 комната 15 — два факела, чомпер, страж.
В отличие от 13/23 (разовый пик на каскаде плит) здесь дорога САМА
статика: Кид и страж стоят, а кадр стоит 801 768 тактов = 1,86
растрового кадра, период 4 растра во всех 866 интервалах прогона.

Фазы: синяя 293 238 (heal 141 048 + логика 152 190), зелёная 320 916
(loose_tick 28 872 + process_trobs 92 448 + redraw_needed 190 260),
циан 187 758.  Впечатление «циан ~150 % кадра» не подтвердилось: за 867
кадров разброс циана 174 такта, это 0,44 растра.

Главная находка — 190 260 тактов на ОДИН тайл (pop_dbg_rdmax_tot = 1).
Пламя факела запекается в ячейке правого соседа, то есть поверх чомпера,
и process_trobs метит соседа (порт set_redraw_anim_right).  Оригинал на
такую пометку рисует ТОЛЬКО слой anim, мы же отвечаем heal 32x64 плюс
полный draw_tile — со всеми слоями, которых пламя не касалось.

Заодно разложена цена одного блита фона (брейкпоинты на резидентных
адресах внутри pop_blit_b, temp0 на входе, 1603 блита): фиксированная
накладная 6 126 тактов на ЛЮБОЙ блит — пролог с IX-фреймом 810,
atlas_image 672, w0_map с чтением шапки 2 400, cd_touch 2 069, unmap 175.
У самого дешёвого блита это 59 % цены, у пламени 16x18 пиксели тянут
лишь 12 %.  Причины ровно те, на которые указал пользователь:
16-битные аргументы там, где хватает 8 бит, и адресация через IX.

perf_registry.md сводит в один отсортированный список всё отложенное из
perf_green_phase (G1-G9), perf_cyan_phase (C1-C7), perf_backlog (1-7),
HEAL-WIDTH и сегодняшние находки — с пометкой замер/модель/гипотеза.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:21:02 +03:00
snark13 0832445614 Уровень 9 прошёл предварительный тест; замер тактов 13/23 без регресса
Уровень 9 (зелье инверсии) — багов не найдено.

Регресс 13/23, 2701 кадр: работа 880 272, синяя 159 822, зелёная 419 562,
циан 380 202 — против 880 170 / 159 774 / 419 520 / 380 244 у 3bcaf51.
Разброс ±100 тактов на 880 000 (0,01 %), циан даже в минус.  Период совпал
кадр в кадр: 4 растра в 24 кадрах, 5 в одном.  Host-тесты 5106 проверок
без расхождений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:49:37 +03:00
snark13 ed7e63a4fa Замер тактов 13/23 после фиксов уровней 3-7: регресса нет
2435 кадров сцены каскада.  Максимумы по секциям: работа 880 170,
синяя 159 774, зелёная 419 520, циан 380 244 — против 878 550 / 158 880 /
419 526 / 379 488 у ec1f384.  Все четыре в пределах шума прогона, зелёная
совпала до 6 тактов.  Распределение периода совпало кадр в кадр: 4 растра
в 24 кадрах, 5 в одном, остальные 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:50:26 +03:00
snark13 671c947ce9 impl_diff: тень спрайтами Кида — осознанное расхождение, не баг
XOR-приём оригинала требует чтения видео-ОЗУ (нельзя) и несовместим с
0xFF-прозрачностью; план — отдельный атлас тени.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:06:19 +03:00
snark13 0cae2cb32a QuickSave: носитель — файл, а не EMM-страница
Пересмотр по вопросу пользователя.  Первая редакция плана рекомендовала
EMM-страницу — ошибка: взвешивала скорость и недооценивала главный
сценарий.

EMM-страница не переживает рестарт программы, а именно рестарт — тот
случай, ради которого QuickSave и нужен: сцену каскада плит на 13/23
воспроизводит ТОЛЬКО ESC → запуск заново (perf_l13_room23.md §1).  Снимок
в ОЗУ там не помогает вовсе.

Доводы за EMM при перепроверке оказались слабыми: лимит манипуляторов DSS
ни при чём (один файл, гард _fd_guard и так стоит), а экономия на пути к
файлу — одна строка.  Разница в скорости некритична: 1,9 КБ на HDD не
заметны на фоне полной перерисовки комнаты при загрузке.

Добавлен шаг QS0 — проверить, что D: вообще пишется из-под MAME: если
образ только на чтение, это меняет весь план, поэтому идёт первым.
Критерий приёмки задачи: сохранить, выйти, запустить заново, загрузить —
и оказаться там же.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:01:14 +03:00
snark13 78b93a6b5e План QuickSave/QuickLoad: разбор SDLPoP + инвентаризация нашего состояния
Только изучение и план, кода нет.

Первое, что выяснилось: в оригинале 1989 года быстрого сохранения НЕТ
вовсе — это enhancement SDLPoP (seg000.c, USE_QUICKSAVE, F6/F9).  Значит
искать в Apple II / MSDOS нечего, и повторяем мы не букву, а устройство.

Что берём у SDLPoP: плоский снимок с ОДНИМ обходом на запись и на чтение
(#define process(x)); совместимость держится строкой версии и ничем больше;
клавиша только взводит флаг, работа идёт между кадрами; состояние отрисовки
не сохраняется вовсе — комната перерисовывается с нуля.

Чем наш случай тяжелее: уровень в EMM-странице, room_modif/trobs — static в
банковом pop_trob.c, ГСЧ у нас ТРИ (pop_t_seed, trob_seed, pop_fight_seed),
и дабл-буфер требует перерисовать после загрузки ОБЕ страницы.

Снимок ≈1,9 КБ, поэтому основной носитель — EMM-страница (мгновенно, мимо
DSS и его лимита манипуляторов), файл вынесен в необязательный шаг QS6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:56:27 +03:00
snark13 f44663040c Регресс тактов на 23/13 после обхода уровней 1-2: изменений нет
Замер 418 кадров, зонды A/C/D/E (база модуля roomtest 0x42AD — совпала с
прошлой сборкой, фикс ушёл в банк).

                работа     синяя   зелёная      циан
af189a1        916 458   142 830   546 900   270 510
40f0d46        873 930   158 874   417 630   379 482
ec1f384        878 550   158 880   419 526   379 488

Фиксы второго уровня на бюджет не повлияли: +4 620 работы и +1 896 зелёной
— шум прогона.  Период: 3 растра в 392 кадрах, 4 в 24, 5 в одном, то есть за
бюджет вылезает только сам каскад.

Синяя и циан в бюджете 400 000; зелёная 419 526 — 1,05x цели и ниже
растрового кадра 430 000.  Остаток на потом: раскол draw_tile на узкие части
и идея G8 (инвалидация соседнего тайла полосой 28 px вместо целых 60).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:49:02 +03:00
snark13 aa845c0362 Зелёная фаза: записана идея G8 — пометка соседа узкой полосой
Идея пользователя 2026-08-17.  При падении плиты помечаются ДВА тайла, и
сосед перезапекается ЦЕЛИКОМ и ПОВТОРНО (draw_tile для него зовётся дважды),
хотя потревожили у него только левые 28 px — там, куда свисает правая грань
упавшего тайла.

В записи разведено, что 60 = 32 свой тайл + 28 СОБСТВЕННЫЙ свес (сужать
нельзя), и сузить можно только пометку «изменился мой ЛЕВЫЙ сосед».  Плюс три
условия: pop_floor_bake общая (кнопка/зеркало/предмет/щебень) и нужен
отдельный вход; вертикальные диапазоны двух запечек НЕ совпадают (20 строк
против 39), поэтому просто снять вторую пометку нельзя; ширину полосы брать
по максимальному свесу.

Брать ПОСЛЕ обхода всех уровней — решение пользователя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:50:56 +03:00
snark13 bb7cf30910 Зелёная фаза: записаны ДВА условия корректности копии второй страницы
Новый раздел §4: копия годится только если (1) запечка ограничена
копируемым прямоугольником — иначе то, что легло вне него, на второй
странице остаётся прежним и страницы расходятся (мерцание торцов, ур.1
к.6, кнопка (0,2)); и (2) содержимое тайла между двумя кадрами не
менялось — у кнопки с идущим таймером пометка обновляется каждый кадр.

Перечёркнут прежний вывод «окно клипа в pop_floor_bake — только вред»:
по скорости да, но оно ОБЯЗАТЕЛЬНО как условие (1).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:05:29 +03:00
snark13 14e183108d Документы по фазам: результаты дня и оставшийся план
Зелёная и циан — раскладка ПОСЛЕ правок, журнал правок с коммитами,
отрицательные результаты с объяснением причины (окно клипа в
pop_floor_bake — три попытки), и что осталось: раскол draw_tile на девять
узких частей по образцу seg008:01C7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:23:38 +03:00
snark13 babd40bc84 Профиль каскада плит ур.13 к.23: три рабочих документа по фазам
Контрольный замер перед оптимизацией (задача OPT-BLIT требовала начать с
него, а не с правок).  Сцена: старт уровня 13, комната 23, шесть плит
сыплются с потолка вразнобой.

Пик логического кадра — 1 437 150 тактов при растровом кадре 430 000, то
есть 5-6 растровых кадров вместо трёх.  По секциям (бюджет 400 000):
синяя 142 830 (в норме), зелёная до 805 000, циан до 632 000.

Что нашлось (всё подтверждено зондами, не гипотезы):

  RDA_CEIL      дрожащая плита-потолок     48 658 x до 6 = 292 000
  RDA_CEIL_GONE запечь колодец            251 023 x до 2 = 619 000
  RD_FLOOR      щебень на месте посадки   198 259 x до 2 = 397 000

Внутри RDA_CEIL: heal 8 491 + draw_tile 39 029, а блитов в этом draw_tile
всего 1,17 на тайл (15-16 тыс.) — то есть ~23 700 тактов чистых накладных.
Подсчёт asm сходится до процентов: у draw_tile 26-байтовый стековый кадр и
513 обращений (ix), при ~46 замеренных тактах на обращение это те же
~23 600.  У blit_b_clip — 22 Б кадра и 211 (ix).

Разбор одного pop_blit_b (157 замеров быстрого пути): atlas_image+w0_map
1 086, ядро libbgi 10 422, pop_cd_touch 4 502 (28%!), unmap+возврат 264.
В mob_render эти три pop_cd_touch к тому же ИЗБЫТОЧНЫ — коридор куска уже
помечен одним вызовом в pop_loose_mob_tick: 81 000 тактов в кадре впустую.

Итог по пиковому кадру: блиты 488 100 (34%), из них «железный» минимум
пикселей ~150 000; остальные ~1 150 000 (80% кадра) — наши накладные.
Узкое место не передача пикселей, а 16-битная арифметика в IX-кадрах.

Ответ на вопрос про uint8_t: просканированы все 109 .atl и наборы
TITLE/PV — максимум по игровому кадру 56x63 (kid3), фон 48x63, спрайты
комнаты принцессы 49x60.  Шире 255 только восемь полноэкранных подложек
титров/сюжета (три 320x200 и пять 256..272), и все рисуются раз на экран.
Значит горячий путь переводится на байтовые габариты целиком, а подложкам —
отдельный банк / прямой libbgi (решение пользователя).

Документы (решение пользователя — вести работу по фазам, чтобы результаты
не теряись между сессиями):

  docs/perf_l13_room23.md  сцена, рецепт воспроизведения, зонды, канал clog,
                           сводка по кадрам, габариты спрайтов
  docs/perf_green_phase.md зелёная фаза: раскладка, позиции G1..G6, журнал
  docs/perf_cyan_phase.md  циан: раскладка, позиции C1..C7, журнал

Отдельно записан рецепт воспроизведения: сцена повторяется ТОЛЬКО
перезапуском программы (ESC → зонды → roomtest).  Возврат в комнату не
годится — упавшая плита уходит в страницу уровня насовсем; рестарт уровня
не годится — гряда доваливается заочно через trob комнаты 17, пока телепорт
уносит Кида; запись отладчиком на бегущей машине затирается тем же trob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 12:22:21 +03:00
snark13 c31930dcad midtable: разобрано, насколько узко оригинал помечает передний слой
set_redraw_fore (seg007:0550) зовут ровно из трёх мест, и нигде нет
пометки «всё»: персонаж помечает прямоугольник своего футпринта
(redraw_at_char, seg003:0576; для Кида — объединение с прошлым кадром),
падающий кусок — соседа справа и второй тайл на границе рядов (draw_mob,
seg007:1147), анимация тайла — один тайл.

Значит пометка переднего слоя НЕ ШИРЕ нашего окна вокруг персонажа, и
полный порт objtable её не отнимает, а формализует.  Главный риск снят.

Побочно найден точный рецепт для MOB-CLIP-RIGHT: оригинал не рисует соседа
поверх куска и не полагается на clip.right — он помечает соседний тайл
парой set_redraw2 + set_redraw_fore, и порядок делает всё сам.  Это же
закрывает «плиту перед колонной».

Рекомендация по итогам: вариант A (полный порт).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:10:55 +03:00
snark13 322d021411 roomnav_skip: достижимость считать обходом от старта, а не входящими связями
Уровень 1 показал ошибку критерия: комнаты 13 и 18 ссылаются только друг на
друга (13.down = 18, 18.up = 13), то есть входящая связь у каждой есть, но
из остального уровня в них не попасть.  Счётчик входящих их пропускал.

Заменено на BFS от стартовой комнаты по связям.  Уровень 1 теперь даёт
ровно {13, 18, 24}, как и должно быть.  Заодно всплыло, насколько мало
комнат реально проходимо на поздних уровнях: 13-й — 11 из 24, 14-й — 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:08:47 +03:00
snark13 69081ae51e Таблица комнат для отладочного телепорта: что пропускать и почему
Посчитано скриптом по res20NN.bin для всех 15 уровней.  Три критерия:
нет входов (ни одна комната не ссылается связями), нет пола (стоять не на
чем) и пол только опасный (обычного пола код 1 нет — пики/расшатанные).

Плюс спецкомнаты, которые пропускать независимо от критериев: seamless
exit 12/23 (меняет уровень), falling exit 6/1, falling entry 7/17,
спуск-через-зацеп 7/14, тень с зельем 5/24 и 13/23+13/16, где вход роняет
гряду плит.  Уровень 15 (защита от копирования) — целиком.

Нужна для полного обхода комнат с первого уровня после порта слоёв.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:04:44 +03:00
snark13 9e1a55bdc3 Падающие плиты: спрайты падающего набора, замеры слоёв, анализ midtable
Спрайты: у падающего куска obj_id = 10 (add_mob_to_objtable, seg007:1170),
и три части берутся из таблиц по этому индексу — 70/74/72, а не 41/43/42.
Мы рисовали плиту В ПОКОЕ, отсюда «цельная ровная» вместо двух половин со
сдвигом правой на пиксель.

Подпорка «перерисовать соседний тайл поверх куска» убрана: она тащила на
плиту чужой узор и окно, да ещё стоила по одной полной отрисовке тайла на
кусок за кадр.  Клип объекта (clip.right = 40) НЕ портирован — единицы поля
не выяснены, буквальные 40 пикселей срезают задний угол плиты.  Цена отказа
— правая грань куска и передние части чужих тайлов его не перекрывают
(MOB-CLIP-RIGHT, BUGS_OPEN.md).

Коридор heal сужен по высоте 32 -> 24: реальный след спрайта 16 px.
Ширина оставлена 64 — из неё используются 62, экономить нечего; заодно в
комментарии зафиксировано, что «выравнивание блока акселератора» ничем не
подтверждается и требует проверки артефактом.

docs/midtable_analysis.md — разбор слоёв оригинала и замеры:
- логический кадр 1 289 526 тактов;
- pop_redraw_needed 978 тактов (0,08 %);
- fore-проход 128 778 тактов (10 %) на ОДНОГО персонажа, но выполняется
  лишь на 4 % кадров — гасит пропуск неизменившегося персонажа;
- максимум объектов на одном тайле 2 (пара из loose_fall), значит
  сортировка внутри тайла бесплатна.

Вывод: единственный риск порта objtable — сохранится ли окно клипа
fore-прохода; до разбора redraw_frames_fore выбирать вариант рано.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 19:56:56 +03:00
snark13 67d62e3e8b Уровни 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>
2026-08-13 18:21:52 +03:00
snark13 948d8f08f2 Переворот: перерисовка вместо отражения, переключение на границе кадра, clip_char
Три правки по следам прогона пользователя на зелье инверсии.

1. ЛИШНИЕ ЯЗЫКИ ПЛАМЕНИ.  pop_flip_screen отражал уже нарисованное копией
   акселератора, а пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию (pop_torch_draw: heal'а
   у него нет, следующий кадр непрозрачно накрывает предыдущий).  Отражённое
   вместе с фоном, оно оказывалось в зеркальной позиции, где накрывать его
   некому — и оставалось навсегда (уходило только после входа в комнату,
   который строит фон с нуля).  Теперь переворот ПЕРЕРИСОВЫВАЕТ комнату тем же
   приёмом, что вход (ENTER-ROOM-FAST).  Это совпадает с оригиналом: SDLPoP на
   need_redraw_because_flipped зовёт redraw_screen(0), а не отражает картинку
   (seg000.c:928).

2. МОМЕНТ ПЕРЕКЛЮЧЕНИЯ.  Зелье выпивается из play_seq, в середине кадра, и
   pop_upside переключался прямо там — остаток кадра рисовался зеркально
   поверх ещё неперевёрнутого фона.  Разделены pop_upside_want (пишут зелье,
   смерть, чит U) и pop_upside (читают слои); переключение — одно место, начало
   кадра, вместе с перерисовкой.  Оригиналу этого не нужно: он всегда рисует в
   offscreen неперевёрнутым и зеркалит только на выводе — расхождение записано
   в docs/impl_diff.md.

3. CLIP_CHAR В ПЕРЕВОРОТЕ.  Граница clip_char приходит в ЛОГИЧЕСКИХ
   координатах (y_clip), сравнивать её с уже отражённым top нельзя: то, что
   логически выше линии, на экране ниже неё.  Теперь тот же срез применяется
   с другого конца — укорачивает кадр снизу (bcut), верх остаётся на месте;
   строки ленты не сдвигаются, потому что зеркальная лента отдаёт их в
   обратном порядке.  Раньше в перевороте клип был отключён совсем, и висящий
   Кид рисовался поверх плиты.

Проверено в MAME: переворот чистый, фон без остатков.  tests-host 6/6.
2026-08-12 23:33:01 +03:00
snark13 f541c0aad9 L9-INVERT: клип поля при перевороте (мусор в бортах)
Найдено пользователем: после телепорта в перевёрнутом виде ниже поля мусор.
Спрайты, торчащие в обычном виде ВЫШЕ поля (полоса кладки у потолка), после
отражения торчат НИЖЕ и лезут в борт с полосой HP.

blit_b_clip при перевороте режет по обеим границам поля всегда, а не по
pop_t_clip_top; быстрый путь pop_blit_b отдаёт клипующему всё, что выходит
за поле.

Проверено в MAME: телепорт по комнатам в перевёрнутом виде — борта чистые,
комнаты (факелы, чомперы, пол) зеркальны.  Переходы между комнатами в
перевёрнутом виде работают.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:16:27 +03:00
snark13 7c1ded340c L9-INVERT: зеркальный слой фона + вход в комнату в банк 8
II.3: pop_upside проведён в единственную точку тайлового слоя (pop_blit_b /
blit_b_clip): позиция через FLIP_TOP, блит — vflip-двойник.  Этим зеркалятся
полная отрисовка комнаты, точечные редрои, trob-анимации, fore-слой и кладка.
В клипованном пути обрезка сверху экрана съедает нижние строки источника,
поэтому кусок пересчитывается как sy' = h - sy - dh.

MEM-COLD2 п.1: enter_room_side/enter_room — в банк 8 (куча 450 -> 1231 Б).
Рабочие массивы комнаты остались в _DATA, в банк уехал только код.

Проверено в MAME (уровень 9, чит U): комната перевёрнута целиком, пол
вверху, дверь и кладка вверх ногами.  tests-host 6/6, size-check OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:43:22 +03:00
snark13 5c015739fa size-baseline: pageflip вырос от новых проверок теста (не регрессия libbgi) 2026-08-12 17:36:19 +03:00
snark13 70c1668e9f ENTER-ROOM-FAST + вынос порядка отрисовки в банк 8
Вход в комнату: одна отрисовка в скрытую страницу, показ флипом, вторая
страница — gfx_copy_page(DIRECT) вместо повторной отрисовки.  Процесс
рисования тайлов больше не виден, обе страницы синхронны (два снимка подряд
идентичны).  Видимую страницу главный цикл теперь перечитывает у железа в
начале кадра: её меняют и вход в комнату, и переворот — оба из банка.

MEM-COLD2 п.2: guard_over_kid с хелперами (objtile_at_char/char_x_left_of/
tile_div_mod) — в банк 8.  Куча 1005 -> 1574 Б, цена — один трамплин на кадр.

tests-host 6/6, size-check OK, в MAME проверены старт уровня 9, переходы
между комнатами и отрисовка стража.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:22:05 +03:00
snark13 b1ef48bd16 L9-INVERT: состояние переворота, зелье типа 4 и pop_flip_screen
Шаг II.1 плана.  pop_upside/pop_upside_dirty (порт upside_down и
need_redraw_because_flipped), ветка case 4 в proc_get_object (только тоггл —
ни вспышки, ни урона, как в оригинале), сброс стартом уровня и смертью Кида.

pop_flip_screen (банк 8) переворачивает уже нарисованное двумя копиями
акселератора: VFLIP в скрытую страницу, показ, DIRECT во вторую; затем
инвалидация слотов персонажей, метки фона и полосы HP.

Чит-клавиша U — порт seg000:0793 (в оригинале переворот тоже на чите): без
неё эффект не проверить, чит-телепорт до зелья в комнате 7 не дотягивается.

Проверено в MAME на уровне 9: комната переворачивается целиком за кадр,
повторное нажатие возвращает.  Персонажи и факелы пока не зеркалятся —
это шаги II.3/II.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:48:08 +03:00
snark13 7fe4c25ad1 libbgi: переворот экрана акселератором (gfx_copy_page + _bgi_flip_rows_raw)
Первый шаг L9-INVERT.  Разведка железа закрыта экспериментом: Port_Y можно
менять между read- и write-триггером, если перед вторым OUT стоит STOP —
приём уже был в проде в _bgi_scroll_cols_raw (указал пользователь), так что
обходной путь через ОЗУ-буфер не понадобился.

- _bgi_flip_rows_raw: построчная копия video->video с реверсом Port_Y
  приёмника (DI/EI бандами по 16 строк, размер блока армируется в каждой
  скобке, убывающий y1 держится SMC-ячейкой);
- gfx_copy_page(area, GFX_COPY_DIRECT|GFX_COPY_VFLIP): копия области из
  неактивной страницы в активную; банк 0x50, то есть приёмник обновляется и
  в VRAM, и в ОЗУ-копии — иначе heal восстанавливал бы старый фон;
- tests/pageflip: 5/5 PASS в MAME (прямая, vflip, рамка, широкая 320 двумя
  проходами, источник цел).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:38:10 +03:00
snark13 52b43b3d81 L9-INVERT: зафиксированы решения по кэшу зеркальных кадров и порядку работ
Кэш — ленивый постраничный, живёт до конца уровня; ENTER-ROOM-FAST делается
шагом 4 на готовом gfx_copy_rect.  Открытых вопросов в плане не осталось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:14:41 +03:00
snark13 e11877f475 План реализации L9-INVERT (docs/l9_invert_plan.md)
Разведка железа первым шагом (смена Port_Y между burst-триггерами и
адресация двух страниц в W3), ядро копии с реверсом Y, vflip-блиты для
row-major, gfx_copy_rect экран-экран; в приложении — состояние, единая точка
пересчёта Y, фон через pop_tile.c, зеркальные кадры персонажей, клип.
Побочной задачей ENTER-ROOM-FAST на том же кирпиче.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:12:33 +03:00
snark13 d7d18aef77 Зелье медленного падения (перо) + цвет пузырьков по типу зелья
L7-FEATHER: тип 3 (уровень 7, комната 1) больше не пустой TODO.

- pop_map.c: pop_feather — счётчик кадров эффекта (POP_FEATHER_FRAMES = 225,
  ванильный порог do_timers seg003:0517; привязку к звуку взять неоткуда).
  fall_accel — ускорение 1 / потолок 4 (seg006:057C) вместо 3 / 33.
  proc_get_object case 3: взвести эффект + зелёная вспышка на 3 кадра.
- pop_kid.c: опкод JMP_IF_FEATHER (0xF7) больше не пропускает адрес
  безусловно — под пером прыгает по нему, то есть seqtbl уводит падение и
  удар в ветки stepfloat/bumpfloat (плавные кадры, без урона).
- roomtest.c: pop_flash_red -> pop_flash_color (жёлтый/красный/ЗЕЛЁНЫЙ);
  сброс pop_feather в pop_start_level (seg003:189).
- Цвет пузырька по ТИПУ зелья (seg008:652), чего у нас не было вовсе:
  3/4 зелёный, 5/6 СИНИЙ, остальные красный.  Mono-блиттера с параметром
  цвета в libbgi нет, поэтому цвет запекается при упаковке: pop_pack_bg.py
  кладёт те же 7 кадров ещё дважды (id 30..36 зелёные, 40..46 синие),
  pop_potion_draw выбирает набор.  Атлас 23 -> 37 спрайтов (+423 Б).
- Синее зелье «−HP» приведено к оригиналу (seg006:1892): своей вспышки не
  ставит (красный кадр даёт общий flash_if_hurt — иначе экран красился
  дважды), а на уровне зелий забирает ПОЛОВИНУ запаса HP.

Такое зелье стоит уже на пройденном уровне 2 (комната 13, тайл (1,3)),
а также ур.8 комн.2 и весь ур.15 — до сих пор оно было красным.

Тесты: phys_feather_fall_is_slow_and_harmless (обычное падение с двух рядов
разгоняется и стоит HP, под пером скорость <= 4 и HP целое); tests-host
5/5 (1727 в [phys]), size-check OK.

Расхождения с ванилью — в docs/impl_diff.md (перо ловит только Кида;
синее зелье без своей вспышки).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:13:29 +03:00
snark13 abe4a83d38 Уровень 5: тень не дерётся, кнопка в шве, узор паласа после зелья
Три бага одной отладочной сессии, все проверены в MAME пользователем.

1. SHADOW-FIGHT-L5. Кид доставал меч, а тень вставала в боевую стойку и
   рисовалась спрайтами стража.  В pop_check_can_guard_see_kid первым
   множителем стояло `Guard.charid != 0`, тогда как оригинал (seg003:702)
   пишет `Guard.charid != charid_1_shadow || current_level == 12`: тень
   боевая только на 12-м уровне.  Оба симптома шли из одной ветки
   control_standing (seg005:352).  Возвращён и пропущенный множитель
   `Guard.direction != dir_56_none`.

2. SEAM-BUTTON-STALE. Кнопка в шве не меняла вид ни при нажатии, ни при
   отжатии.  Сигнатура редроя шва сравнивала room_modif, а у кнопки modif —
   это индекс LINKLOC, константа уровня: нажатие живёт в doorlinks2, и в
   сигнатуру не приходило никогда.  seam_row_sig подмешивает бит нажатости.
   Оригинал шов в этом случае не перерисовывает вовсе — расхождение
   осознанное, записано как D-2 в docs/impl_diff.md.

3. BUG-POTION-STRIPE. Синяя лента паласа пропадала на месте выпитого зелья
   и не возвращалась даже после перезахода в комнату.  У оригинала
   curr_room_tiles — сама таблица уровня, у нас fg живёт в двух местах, и
   страницу правил главный цикл уже после pop_process_trobs.  В этот зазор
   trob зелья крутил фазу пузырька поверх обнулённого модификатора:
   bubble_next_frame(0) = 1, а для пола в паласе modif 1 означает «узор не
   рисовать» (seg008:499).  do_pickup теперь правит страницу уровня сразу.
   Тем же лечится меч: animate_sword делал `--mod[tp]` из нуля.

tests-host 5/5 (добавлена заглушка pop_level_set_tile), size-check чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:24:15 +03:00
snark13 5348feb5f4 Доски приведены в соответствие с кодом; bug_list/bug_closed → BUGS_OPEN/BUGS_CLOSED
Доска отставала от кода на три задачи — планировать по ней было нельзя.
Сверка проведена грепом по исходникам, а не по записям:

- L4-MIRROR ЗАКРЫТА: шаги 1-5 сделаны и проверены пользователем в MAME
  (зеркало в атласе, постановка тайла, отражение, прыжок сквозь зеркало с
  рождением тени, левый клип тени).  Протокол с разбором решений — в архиве;
- L3-CHOMP и L3-SKEL закрыты ещё 2026-08-08/07 (коммиты dc0bd47, 4d4323f,
  db4106a, 1461ed5), на доске значились как предстоящие;
- тайлсет palace (шаг 2 levels_plan) в коде есть целиком — pop_bg_load(type),
  pal_*.atl, дворцовый wall_pattern, решётки 25-29 и в tile_table, и в
  коллизии (tile_is_floor совпадает с seg006:0628);
- в GUARD-PHYS остаток пересобран по факту: check_chomped_guard сделан,
  скелет в check_guard_fallout сделан, ветки ТЕНИ нет — она уехала в L5-SHADOW.

Приёмки: по решению пользователя уровни 1-4 приняты SMOKE-тестами, полные
обходы всех комнат делаются по готовности ВСЕХ уровней — L3-PASS/L4-PASS как
отдельные задачи отменены, вместо них политика приёмок в архиве.

Новая цель — уровень 5.  Инвентарь res2005.bin: НИ ОДНОГО нового тайла, всё
портировано на уровнях 1-4.  Единственная новая механика — спецсобытие «тень
крадёт зелье» (комната 24): заведена задача L5-SHADOW с портом по SDLPoP
(check_shadow / do_init_shad / do_auto_moves + shad_drink_move /
autocontrol_shadow_level5 + ветка тени в check_guard_fallout), включая
готовые константы и то, что у нас уже есть под это.

Заведён MIRROR-FG-STALE (низкий): place_mirror пишет тайл в данные уровня, но
не в снимок room_fg, по которому работает коллизия — если зеркало поставлено,
пока игрок В комнате 4, оно невидимо для коллизии (тень не родится).  В
обычном прохождении недостижимо: дверь выхода в другой комнате.  Записан
точный сценарий воспроизведения читом ROOMNAV и фикс на несколько строк.

Правило «в _OPEN только незакрытое» теперь выполняется буквально:
- bug_list.md → BUGS_OPEN.md, bug_closed.md → BUGS_CLOSED.md (ссылки
  обновлены во всех документах и в комментарии pop_trob.c);
- из TASKS_OPEN убраны блоки закрытых задач (L3-CHOMP, L3-SKEL, L3-PASS,
  L4-MIRROR, DRAW-CHAR), справка по связности комнат уехала в архив;
- из BUGS_OPEN убраны 8 строк таблицы закрытых багов, закрытый T-2 (уехал в
  BUGS_CLOSED) и раздел «уровень 3 — неначатые задачи» (обе записи закрыты);
  сводная таблица пересобрана по реально открытым записям.

Все внутренние ссылки проверены скриптом: битых якорей 0.  make size-check
OK (65 программ), tests-host 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:10:10 +03:00
snark13 860468f3c5 libbgi: NOT_PUT тоже через акселератор + изыскания по виду Тени
NOT.  У акселератора нет режима «инвертировать буфер»: буфер меняется
только на ЧТЕНИИ и только опкодами AND/OR/XOR (HL) (драйвер MAME,
update_accel_buffer), а `CPL` автомат вообще не распознаёт — инвертируется
регистр CPU, не буфер.  Зато ~src = src XOR #FF, поэтому NOT собирается из
уже имеющегося: буфер := src, XOR-burst по константному блоку единиц
(common/_bgi_ones256.c), запись.  Ядра _bgi_blit_{rows,cols}_not_raw.c;
между burst'ами меняется HL, поэтому каждая смена — под СТОПом (иначе fetch
операнда перезапустит burst).  В колоночном варианте второй OUT Port_Y не
нужен: op-burst идёт по блоку единиц горизонтально, а Port_Y шагает только
вертикальный.

Наружу — тем же op-параметром (GFX_OP_NOT), диспетчер один раз на вызов, в
цикл по полосам/колонкам не заходит.  putimage лишился попиксельного пути
ЦЕЛИКОМ: все пять операций BGI идут через акселератор и клиппируются
одинаково.  tests/accop дополнен T9/T10 (NOT строками и колонками) —
10/10 PASS в MAME; tests/bgi_img P1/P2 по-прежнему PASS.

tests/convbench (новый) — замер побайтной конвертации атласа
«прозрачный #FF -> 0x00» (источник для XOR-блита): 145.3 такта/байт, то
есть 2.5× от 59 номинальных T-states цикла.  0.11 с на страницу 16 КБ,
3.2 с на все 28 страниц, 1.3 с по фактическому объёму данных (186 КБ).

applications/PoP/docs/shadow_render.md — вид Тени (два блиттера OR+XOR)
ОТЛОЖЕН по решению пользователя: пока рисуем обычной копией атласами Кида.
В документе собрано, почему в лоб не выходит (XOR несовместим с
прозрачностью #FF; операция читает ОЗУ-копию, поэтому два прохода
оригинала вырождаются в один XOR — нужен однопроходный композит
s | bg ^ s(сдвиг)), замер стоимости источника и четыре варианта.  Ключ к
выбору — список кадров, которыми тень реально пользуется; снимать по факту,
когда пойдут уровни 5/6/12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:41:06 +03:00
snark13 ec3cca5e1e Луч видимости стража: колонка из таблицы вместо деления (Кид у шва)
tile_at_kid (guards.c) считала колонку честным / и %, хотя резидентная
POP_TILE_DIV — это и есть tile_div_tbl оригинала, и остальной порт давно на
неё переведён.  У SDCC z80 пара / и % над int это __divsint плюс __modsint,
который внутри снова зовёт __divsint — ~5 400 тактов на вызов.

Зовут её в ЦИКЛЕ по колонкам между стражем и Кидом
(check_can_guard_see_kid, seg003:761).  Когда Кид у шва, его curr_col = −1,
луч тянется через всю комнату: замер дал ВОСЕМЬ пар делений за кадр,
около 43 000 тактов = 10 % растрового кадра, в фазе логики.  После фикса
таких вызовов не остаётся.

Как ловилось: брейкпоинт на __divsint с печатью адреса возврата дал
ret=C033 восемь раз за кадр; остановка на нём с dasm при замапленном банке
показала HL−65 / ld de,#14 / call __divsint по смещению 0x24 банка 1.

Снята и ложная тревога из прошлого коммита: пролог pop_char_fore на шве НЕ
разбухает до 134 730 — это была ошибка зонда (адрес fore_tile в банке
совпадает с кодом других банков, в интервал попадали чужие срабатывания).
Чистый замер: пролог 16 950, как и в середине комнаты.  Урок записан в
«Как мерить» в docs/perf_backlog.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:53 +03:00
snark13 08ac38ce3d Пункт 0: отсев кладки по окну fore-клипа; закрыт BUG-CHEAT-FIGHT-1
Три шага пункта 0 из docs/perf_backlog.md.

1. Ранний выход из wall_pattern_palace по окну fore-клипа: узор целиком
   лежит в x [xh*8, xh*8+32), y [dmy-59, dby], и если окно его не задевает
   — возврат до первой заливки.  Замер: от входа в узор до конца всего
   прохода 6 055 тактов вместо ~60 000 на тайл.
2. Отсев КАЖДОГО декаля (wp_blit) по реальному габариту.  pop_blit_b тоже
   отсеивает до маппинга страницы, но по заведомо большему 64x64 — куски
   кладки высотой 3..12 px он пропускал и платил полный
   atlas_image + gfx_w0_map (~9 760 тактов на блит), чтобы там обнаружить,
   что рисовать нечего.  Размеры сняты из каталогов атласов и заданы верхней
   границей по группе.
3. То же для ПОДЗЕМЕЛЬЯ: ранний выход wall_pattern (габарит выше — левая
   марка уходит на dby+POP_YOFF-67) плюс wp_blit на RNDBLOCK, обоих
   разделителях и обеих марках.

Замер (уровень 4, комната 18, Кид сдвигается читом ] по пикселю, skip
выключен, шесть тайлов в fore-окне, три из них — дворцовая стена):
тайл стены ~12 700 вместо 60 000-74 000, fore-проход целиком 70 681 вместо
171 693, работа за кадр 419 839, период 3 растровых кадра вместо 4.
Уровень 1 (подземелье) проверен снимком — кладка, марки, разделители на
месте.

Остаток в проходе — пролог pop_char_fore 17 208 тактов (пункт 1 backlog'а).
Отдельно записано: на ШВЕ пролог разбухает до 134 730, причина не разобрана.

BUG-CHEAT-FIGHT-1 (заведён 2026-08-07) закрыт по своему же плану: ветка
ROOMNAV после kid_init/pop_kid_hp_reset гасит состояние схватки
(Kid.sword = SWORD_0_SHEATHED, holding_sword, offguard, guard_refrac).
Меч в инвентаре не теряется.  Это болезнь чита: в оригинале телепорта между
комнатами нет и в режим боя без соперника попасть нечем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:24:27 +03:00
snark13 4718bff767 Уровень 4: skip по маске тайлов + порт loose_land, ворота 0xFF, пламя в фон
ОПТИМИЗАЦИЯ.  Метка «фон трогали» была одним union-прямоугольником на
страницу, и три факела комнаты склеивались в полосу x 40..280 на всю
комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки
и каждый кадр платил полным fore-проходом.  Теперь это маска тайлов
(uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница),
проверка — три AND через резидентный pop_cd_hit.  Гранулярность тайла — это
гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe;
подтайловое уточнение там только по высоте, wipe_heights).  Замер на (1,7):
циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3
растровых кадра.

BUG-LOOSE-BUTTON-1.  Упавшая плита не нажимала кнопку.  Три слоя: порт
loose_land не звал trigger_button вовсе; pop_room_col_landing считала
площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки,
зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего
ворота начинали открываться, пока она ещё в воздухе.  Нажатие в оригинале
ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ»
(modifier 0xFF), и кнопка съедается.  Посадка в комнате снизу переехала на
новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже
поля).

BUG-GATE-FF-1.  0xFF был перегружен: сторожевое «тайла нет» в gate_modif и
живое «открыто навсегда» из trigger_gate.  gate_passable заворачивал Кида в
воротах, нарисованных открытыми.  Мёртвая ветка убрана.

BUG-TORCH-CHOMP-1.  Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и
стирал пламя факела (0,6), которое рисуется в ячейке правого соседа.  Фикс —
запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем
канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера
сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале.
Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE.

Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл
tiles_5_stuck в атласе.

Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1)
-> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host —
все 5 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:52:04 +03:00
snark13 93aa51db45 Профиль дворца: циан = fore-проход, 27% кадра на ноль пикселей
Разбивка логического кадра брейкпоинтами (уровень 4, Кид неподвижно на
(1,7)): работа 517 609 тактов = 120% растрового кадра, период 4 кадра.
Циан (PROF(6)) — 233 515 = 54% растрового кадра, из них
pop_char_fore(KID) = 171 693.

Внутри fore-прохода шесть тайлов, и два из них — СТЕНА ряда 2 под ногами
Кида — стоят 74 310 и 65 553.  Дворцовая кладка на тайл: 6 wpp_fill
(~3 100) + 5 pop_wall_b (~9 760) ≈ 60 000.  Окно клипа в этот момент
x 229..241, y 106..147 (прочитано из pop_t_fclip_*), верхний кусок узора
стоит на y=157 — не пересекается вовсе, тайл (2,6) промахивается и по x.
То есть 139 863 такта за кадр рисуют ноль пикселей; снятие уводит период
с 4 растровых кадров на 3.

Записано пунктом 0 в docs/perf_backlog.md с тремя вариантами лечения.
Состав узора сверен с SDLPoP (seg008.c:1943) — порт дословный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:31:45 +03:00
snark13 c8fe0bd37a pop_blit_b: быстрый путь без клипа + backlog отложенной оптимизации
Клипованный путь вынесен в отдельную функцию blit_b_clip: под его девять
16-битных локалей SDCC заводит кадр IX, и за этот кадр платили ВСЕ блиты
фона, включая те, где клипа нет вовсе (весь фон вне fore-прохода — факелы,
зелья, перерисовка тайлов, у них pop_t_fclip_on == 0).  Быстрый путь идёт
сразу в gfx_blit_noclip.

Замер: pop_torch_draw 41 778 -> 31 218 тактов на факел (часть разницы —
прошлая правка pop_cd_touch; чистый вклад этой ~6 300 на блит).  Поведение
не изменилось: клипованная ветка перенесена дословно.

docs/perf_backlog.md — отложенные идеи с измеренной ценой (футпринт из
физики 11 574, размеры ленты из каталога атласа, один map/unmap на группу
блитов, единый проход по тайлам как redraw_needed_tiles, objtable,
отложенные таблицы back/mid/fore) плюс раздел «как мерить»: wait-state'ы
дают 2,4x к справочным тактам, кадр 430 000, адреса символов меняются
после каждой пересборки, сцена между сессиями не воспроизводится.
Там же — что уже проверено и НЕ сработало.

tests-host: 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:05:42 +03:00
snark13 8c4bc4f621 Оптимизация логики: окно коллизии как в оригинале + деление таблицей
Замерено брейкпоинтами в MAME (уровень 1 комната 1, Кид стоит у факела).
Калибровка, без которой цифры не сходятся: такт totalcycles != номинальный
T-такт Z80, wait-state'ы ОЗУ Sprinter дают ~2,4x (get_tile 574 против 1422).

1. Окно перебора коллизии — как у оригинала (left_checked_col..right_checked_col,
   seg004:0047), было: все 14 колонок каждый кадр.  Признак годности слота у
   нас дешевле оригинального: не массив номеров комнат с очисткой, а границы
   окна, которые move_coll_to_prev переносит в prev вместе с флагами; бамп
   считается по пересечению двух окон.  check_chomped_flags тоже ограничен
   окном, иначе протухшие слоты дают фантомный перемол.

2. get_tile_div_mod — таблицами tile_div_tbl/tile_mod_tbl (seg006:702), было
   /14 и %14.  SDCC разворачивал это в __divsint + __modsint, а __modsint
   внутри зовёт __divsint ещё раз: 5 400 тактов на вызов, 13 вызовов за
   кадр = 16 % кадрового периода на «в какой колонке точка».

3. get_row_collision_data: ряд разрешается один раз на весь перебор (было —
   get_tile на каждую колонку, 1 422 такта), грань идёт шагом TILE_SIZEX как
   в оригинале, wall_type таблицей вместо switch.

Итог: check_collisions 60 888 -> 42 750, физика Кида 100 578 -> 67 518,
синяя полоса ~60 % -> ~30 % кадрового периода.  Профиль остатка и следующие
цели (отрисовка Кида 47 %, process_trobs 21 %) — в TASKS_OPEN.md#draw-cost.

tests-host: все 5 наборов прошли.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 14:38:08 +03:00
snark13 9234c03020 BUG-GATE-PASS-1: история флагов коллизии переживает боковой переход
Оригинал (seg004:0004) индексирует флаги перекрытия колонкой ВНУТРИ
разрешённой комнаты и хранит рядом её номер, поэтому решётка комнаты 8
остаётся в своём слоте и после перехода 8->6: переход флага 0->1 виден,
bumped() срабатывает.  У нас индекс — колонка отрисованной комнаты, тот же
тайл менял слот, и enter_room вынужден был выбрасывать историю целиком —
на кадре входа бампа не было, и Кид с разбега уходил сквозь закрытые ворота.

Вариант B (сдвиг вместо тега комнаты): при БОКОВОМ переходе история не
выбрасывается, а перенумеровывается на 10 слотов.  check_leave двигает x
ровно на ∓140 = 10 тайлов, координата грани едет на те же 140 вместе с
габаритом Кида — сами флаги инвариантны, меняется только номер слота.
Сдвигаются curr/above/below (prev на следующем кадре всё равно перезапишет
move_coll_to_prev), освободившиеся слоты = 3 «уже перекрывал».
Вверх/вниз и прочие входы в комнату — по-прежнему полная инвалидация.

Дословный вариант A (10 слотов + массив номеров комнат) не взят: он тянет
за собой сужение окна перебора колонок, то есть отказ от FIX_COLL_FLAGS.
Заведён docs/impl_diff.md — список осознанных расхождений с SDLPoP; правило
«фиксировать расхождение» в обоих CLAUDE.md теперь указывает туда.

tests-host: все 5 наборов прошли.  Приёмка в MAME впереди.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 12:25:38 +03:00
snark13 14190f0210 MEM-BANK2 шаг 3: холодная половина слоя фона в банк 7 (90.4% -> 35.8%)
pop_bg.c разрезан по ЧАСТОТЕ вызова, а не по размеру:

  pop_bg.c   (банк 2)  горячее  fore-проход, оверлеи, кладка, клип
  pop_room.c (банк 7)  холодное draw_tile, точечные перерисовки, mob,
                                загрузка атласов

Стык — три тонкие __banked-обёртки (wall_pattern, wall_pattern_reset,
draw_gate_back): тела остаются непомеченными, поэтому горячий fore-проход,
зовущий wall_pattern до девяти раз за кадр, платит ноль, а трамплин
достаётся только холодному пути — ~40 вызовов на вход в комнату, 26 000
тактов = 0.06 кадра РАЗОВО.  Общее состояние (pop_loose_modif, pop_ceil_modif,
obj_row/obj_col) писучее, лежит в _DATA/W2 и видно обеим половинам.

Заодно удалена мёртвая potion_bubble (169 Б).

Грабля: n_banks объявляет само приложение (roomtest.c), а не sprinter-cc.
Восьмой банк без правки константы линкуется молча, _bank_pages[7] остаётся
0xFF, и программа встаёт намертво до первого кадра.  Диагноз снят дампом
_bank_pages из MAME.

Замер (ALLOCS=3000): BANK2 11 942 -> 5 872, BANK7 6 035; _CODE и куча не
тронуты (22 556 / 4 301).

Проверено: tests-host зелёные; построчная сверка pop_bg.c+pop_room.c против
дорефакторного pop_bg.c — ни одной строки логики не пропало; 8 комнат в MAME
до/после совпали попиксельно по активному экрану (различия только в фазе
анимации факелов и кадре Кида); живой прогон с переходами комнат, боем и
воротами сверен контрольным запуском HEAD-бинаря.

DRAW-COST поднят в приоритете: замер пользователя (ур.1 комната 3, страж
убит, Кид стоит) — синяя 80%, зелёная 20%, циан 110%, итого ~210 %
кадрового периода В ПОКОЕ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 16:28:02 +03:00
snark13 bbf91d10ee DRAW-CHAR: отрисовка одна на всех Char; разгрузка банка 2 (90.4% -> 72.9%)
DRAW-CHAR.  Отрисовка персонажа сведена к одному набору функций над Char —
как физика после GUARD-PHYS.  В оригинале add_kid_to_objtable (seg008:22F0) и
add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются
окном (loadkid/loadshad), набором спрайтов и типом объекта, а
redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе.

  pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот
  POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из
  любого банка без трамплина.  Проход окклюзии тоже один
  (pop_fore_over_char), pop_fore_over_kid больше нет.

Починилось само (расхождения, которые и были ценой дублирования): у
соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг
«мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА.

Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ.
Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация
заведена отдельной задачей DRAW-COST.

MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h).
Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а
const-таблицы — в странице банка, из другого банка их не прочитать; трамплин
же выбирается объявлением, то есть __banked на листе бьёт и по горячим
вызывающим (654 такта).  W1 замаплено всегда — оттуда обе половины зовут
листья прямым call и читают таблицы напрямую.

MEM-BANK2, шаг 2: дедуп внутри банка.  wall_pattern 808 -> 394 и wall_rnd
786 -> 654: четыре ветки по виду стены отличались только набором кусков и
числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE,
порядок вызовов prandom сохранён дословно.

Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии
в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт
макро-инициализатор, массив определяет один pop_kid.c.

Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556,
куча 4301 Б.  tests-host зелёные (65/39/53/1723/1); в MAME комната 1
совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 —
та же раскладка кладки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:25:53 +03:00
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