Банк 2 (pop_bg + pop_gdraw) подошёл к границе страницы вплотную: 16 021 из
16 384, свободно 363 байта. А расти ему ещё есть куда — тайлы поздних
уровней, чомперы, зеркало, анимации смерти стража.
pop_gdraw.c уехал в банк 4 (n_banks 3 -> 4):
банк 2 16 021 -> 13 792 (84.2 %, свободно 2 592)
банк 4 2 236 (13.6 %, свободно 14 148)
Цена: pop_fore_over_char стал кроссбанковым, поэтому помечен __banked —
один трамплин (~654 такта) за кадр, других вызывающих у него нет.
pop_fore_set_clip уже был __banked, так что там ничего не изменилось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Каркас предметов уже был (check_get_item/do_pickup/proc_get_object), пустой
оставалась только ветка зелий. Порт seg005 get_item + seg006
proc_get_object:
- pop_get_item_action теперь отдаёт 3 = «пить» и делает do_pickup с ТИПОМ
зелья, который лежит в старших битах модификатора тайла (modif >> 3);
- pop_ctrl на код 3 запускает seq_78_drink;
- эффекты: тип 1 (здоровье) — +1 HP через hitp_delta и красная вспышка,
причём как в оригинале только если HP не полные; тип 5 («злое») — −1 HP.
Типы 2/3/4/6 (жизнь, перо, переворот, открыть ворота) — свойства поздних
уровней, портируем вместе с ними.
Вспышка фона получила цвет: меч даёт ярко-жёлтую (было), зелье — красную
(flash_color оригинала; двух значений достаточно, других в игре нет).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Смерть стража теперь персистентна между входами в комнату — по механизму
оригинала, а не отдельной таблицей «убит/не убит». В SDLPoP массивы
level.guards_* лежат в ОЗУ и движок их ПЕРЕПИСЫВАЕТ: leave_guard (seg002:02F5)
кладёт туда позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq;
enter_guard, увидев непустой seq_hi, поднимает стража прямо в этой
последовательности и по кадру смерти (185/177/178) ставит alive = 1.
У нас уровень лежит в EMM-странице только на чтение, поэтому в W2 добавлена
живая копия — 6 байт на комнату (tile/dir/x/skill/seq_lo/seq_hi):
- pop_guard_leave() в начале enter_room запоминает уходящего стража;
- pop_guard_enter поднимает труп сохранённой последовательностью И
сохранённой X (pos_guards пересчитывает её из колонки только при загрузке
уровня, дальше ею владеет leave_guard — иначе тело прыгает в центр тайла).
ГРАБЛИ: guards_seq_lo/hi в ФАЙЛЕ уровня не используются, там 0xFF во всех
комнатах (оригинал чистит их в reset_level_unused_fields). Прочитав их как
есть, я скормил интерпретатору curr_seq = 0xFFFF, и приложение зависало —
бордюр оставался синим, цикл не доходил до vsync. Живая копия стартует
нулями: 0 = «поднимать стандартной стойкой».
Меч Киду больше не выдаётся автоматически: DEBUG_SWORD_ROOM убран, вместо
него чит S (выдать меч). Штатный путь — подобрать с пола.
Проверено в MAME: чит K убивает стража, уход из комнаты 3 и возврат —
тело на месте, страж не воскресает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Регрессия от окна Char вокруг control() (cf06896): диспетчер работает с
копией Char, а часть его действий у нас исполняет pop_map (pop_down_action,
pop_jump_up_seq, safe_step, зацеп) — и пишет ПРЯМО в Kid, потому что на Char
он ещё не переведён. Завершающее `Kid = Char` затирало эти правки:
выравнивание x и ряд терялись, и спуск с уступа через вис не срабатывал —
Кид просто приседал.
pop_savekid_state теперь копирует только то, что диспетчер реально меняет
у персонажа: curr_seq и sword. Когда pop_map переведём на Char, вернётся
полное копирование — в комментарии это зафиксировано.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Боёвка (порт seg002/seg006):
- check_sword_hurting / check_hurting / check_sword_hurt / hurt_by_sword /
take_hp через дельты; do_delta_hp сводит их раз в кадр;
- парирование (justblocked), refractimer после ранения стража;
- смерть по seq_71_dying — через неё же теперь работает чит K: страж
действительно погибает, а не замирает на месте;
- парные окна Char/Opp: loadkid_and_opp / savekid_and_opp /
saveshad_and_opp.
Индикаторы HP (порт draw_kid_hp / draw_guard_hp): Кид слева, страж справа.
Перерисовка ТОЛЬКО при изменении числа и тогда на ОБЕИХ страницах
дабл-буфера (счётчик hp_todo, иначе на второй странице осталось бы старое
значение и мерцало через кадр); pop_hp_invalidate при входе в комнату, где
фон перерисован целиком.
Чит I — бессмертие Кида (нашего изобретения, в оригинале его нет).
Перекрывает и путь «безоружного закалывают насмерть»: тот идёт мимо HP, и
без этого чит бесполезен ровно там, где нужен.
ДВА НАЙДЕННЫХ БАГА:
1. Полосу HP блитил row-major примитивом, а атласы Кида и стража хранятся
COLUMN-major (ради бесплатного флипа) — марки выходили транспонированными.
Теперь колоночный блит, стрелки как в оригинале.
2. Кэш кадра для ОТРИСОВКИ заполняли pop_savekid/pop_saveshad. Любое окно
Char БЕЗ play_seq — а это оба окна боёвки — записывало Киду кадр, который
принадлежал СТРАЖУ, и kid_draw искал этот image в атласе Кида, рисуя
произвольную позу. Владельцем кэша стал load_frame: он один знает, чей
кадр загружен (по Char.charid).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт 2 плана закрыт: страж не только замечает Кида, но и идёт к нему и
дерётся. Порт по SDLPoP, диспетчер общий — ИИ выставляет те же control_*,
что и клавиатура игрока.
guards.c (банк 1), порт seg002:
- autocontrol_guard_active (737) + kid_in_sight (0A93) + kid_armed (0AC1)
+ kid_far (09CB);
- guard_advance / guard_block / guard_strike с таблицами вероятностей по
12 градациям мастерства (seg002:26..38), бросок prob > prandom(255);
- move_2_backward / move_3_up / move_6_shift / move_down_back;
- таймеры justblocked / kid_sword_strike / guard_refrac убывают раз в кадр
в autocontrol_opponent, как в оригинале.
pop_ctrl.c, порт seg005: control_with_sword (964), swordfight (0CDB),
sword_strike, parry, forward_with_sword, back_with_sword. Ветвление у
Кида и у соперника разное — соперник блокирует только на кадре 152, Кид
ещё и по 153 (и тогда последовательность прокручивается сразу).
pop_guard.c: guard_skill из данных уровня (12 градаций, вне диапазона -> 3),
HP по get_guard_hp (extrastrength[skill] + tbl_guard_hp[уровень]),
собственный сид бросков pop_fight_seed — иначе перерисовка стены сбивала бы
решения стража.
char_opp_dist переехал из банка в pop_kid.c: он нужен по ОБЕ стороны
банковой границы — и ИИ, и диспетчеру боёвки.
Проверено в MAME (комната 3): страж проходит комнату, встаёт в дистанцию и
машет мечом, позы меняются. Урона пока нет — HP-учёт и check_hurt
следующим шагом, без них бой не заканчивается.
Бюджет В БОЮ (175 кадров): 412 224 – 421 068 = 0.958–0.979 кадра.
В покое было 400 800. Запас в худшем кадре ~9 000 — тесно, но в один
кадр укладываемся; оптимизация отложена сознательно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Тексты обеих Z80-процедур, разбор их устройства и качества, почему НЕ берём
8-битный RND Apple II (вырожденные младшие биты — раскладка кладки читает
prandom(1), вышла бы шахматка), и главное — сколько это реально даст.
Потолок выигрыша 2 814 тактов за кадр (0.65 %): тело генератора уже не
основной расход, остаются обёртка pop_prandom, pop_rnd_fit и ABI вызова.
Поэтому первый шаг, если упрёмся, — слить приведение к диапазону в ту же
asm-процедуру (один call вместо трёх), и только потом менять генератор.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Возврат к БИТ-В-БИТ генератору оригинала по умолчанию (POP_PRANDOM_EXACT=1):
по нему проще отлаживать и сверять картинку с эталоном. Чтобы это не
стоило процента бюджета, сам шаг LCG переписан на Z80-ассемблере —
единственное место в порте, где это сделано, с явного разрешения.
Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
0x343FD) — 17 удвоений, три сложения и одно вычитание; величина 3*s,
нужная в конце, попадается по дороге на втором шаге.
Проверка в ДВА этапа:
- схема на хосте: horner(s) == s*214013+2531011 на 3 000 000 сидов;
- сама asm-транскрипция на живой машине: breakpoint на pop_prandom,
11 последовательных состояний сида из MAME — каждый переход совпал с
s*214013+2531011 бит-в-бит.
Замер, комната 3, 175 кадров (медиана кадра / prandom->torch_draw):
C, бит-в-бит (16-бит половины) 403 632 / 10 933
C, xorshift16 + шаг Вейля 397 986 / 7 927
asm, бит-в-бит 400 800 / 9 331
То есть asm вернул половину разрыва (2 832 такта за кадр), сохранив
совместимость с эталоном. Ветка xorshift оставлена под
-DPOP_PRANDOM_EXACT=0 как запасной ход — брать её имеет смысл, только
если не хватит последних 2 800 тактов.
Итог оптимизационного круга: 416 154 -> 400 800 (0.968 -> 0.932 кадра).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью на 32-бит сделан ПО ASM, а не по коду (искали и безымянные
временные): во всём приложении был ровно ОДИН 32-битный вызов —
__mullong в pop_prandom. Замер в MAME: 8 430 тактов на вызов, два
вызова за кадр. Прочие библиотечные вызовы 16-битные (__divsint 16,
__modsint 12, __moduchar 8, __divuchar 5).
1. pop_prandom. Состояние 32-бит -> две 16-битные половины. Два
генератора, выбор через POP_PRANDOM_EXACT:
- 0 (по умолчанию) — xorshift16 + шаг Вейля, без единого умножения;
- 1 — LCG оригинала бит-в-бит, посчитанный половинами (для сверки
картинки с эталоном).
8-битный RND Apple II (5*x+23 mod 256) НЕ взят: у LCG по модулю 256
вырождены младшие биты (бит 0 просто чередуется), а раскладка кладки
берёт как раз prandom(1) и prandom(4) — вместо шума вышла бы
правильная шахматка. Шаг Вейля ещё и убирает ноль как неподвижную
точку xorshift (сид кладки вполне может быть нулём).
Бит-в-бит эквивалентность half-word версии проверена на хосте:
70 000 сидов x 8 шагов + 7 крайних сидов x 2000 шагов.
Остаток 0..maxv: делитель степень двойки — маска вместо __moduint.
2. libbgi: gfx_blit_cols_part_noclip — column-major блит без клипа
(пара к gfx_blit_cols_part, как gfx_blit_part_noclip к
gfx_blit_part). Клипающий вариант платит ~5 622 такта подготовки на
КАЖДЫЙ вызов независимо от того, вылезает край (замер: подготовка
5 622 против 13 596 на сам accel-проход). Kid, страж и клинок
выбирают путь по pop_onscreen_cols. size-check: роста нет.
Бюджет (175 кадров, комната 3, медиана):
было (после клинка) 416 154 0.968 кадра
стало 397 986 0.926 кадра
Разница между генераторами, замер на одинаковой сборке:
xorshift16 + Вейль 397 986 prandom->torch_draw 7 927
бит-в-бит LCG 403 632 prandom->torch_draw 10 933
то есть точность обходится в 5 646 тактов за кадр (1.3 %).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Меч в оригинале ОДИН на всех: и Кид, и страж рисуют клинок из chtab_0
(add_sword_to_objtable, seg006:1798). Раньше sword.atl содержал только
кадры подъёма/ножен (sword_tbl 35..42), поэтому у стража меча не было
видно вовсе.
- pop_pack_kid.py пакует chtab_0 целиком (id 0..33, 5168 Б), индекс в
атласе = id;
- pop_extract_kid_data.py вытаскивает sword_tbl (53 строки) в kid_data.h
макро-инициализаторами — таблица ложится в один TU, а не в каждый;
- pop_sword_draw (pop_kid.c) — общая точка отрисовки клинка с полным
условием оригинала (кадры 229..237 ИЛИ меч обнажён ИЛИ живой страж);
зовут и kid_draw, и pop_guard_draw.
Раздельный heal накладных спрайтов. Клинок и брызги урона раньше
объединялись в один прямоугольник с персонажем, а объединение почти вдвое
больше суммы двух (клинок уходит вперёд-вверх) — heal же стоит ровно по
площади. Теперь у накладных свой прямоугольник и свой gfx_heal, а
объединение осталось ТОЛЬКО для окна fore-клипа: там это 4 сравнения без
рисования, но покрыть клинок обязано, иначе он полезет поверх столба.
Отладка: DEBUG_SWORD_ROOM — Киду выдаётся меч при входе в комнату 3
(там страж), чтобы не бегать за ним в комнату 15.
Бюджет (225 кадров, комната 3): 415 284 – 416 370 = 0.966–0.968 кадра.
Против 384 168 – 384 636 до этого шага, то есть +31 700. Разложение по
фазам (медианы): process_trobs 83 190, pop_guard_draw 92 483 (спрайт
28 763 + клинок 23 820 + fore_over_char 39 906), kid_draw 59 089, fore
поверх Кида + борта 47 483, heal 40 068, логика 76 338, ввод 13 050.
Видно, что клинок 21x8 стоит почти как спрайт стража — это фиксированные
накладные расходы клипающего блита, а не пиксели; лечится noclip-путём
для column-major (см. gfx_blit_noclip_fast). Отдельным шагом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первый самостоятельный кусок ИИ стража (план, пункт 2). В оригинале у
соперника нет своего диспетчера: autocontrol_* (seg002) выставляет те же
глобалы control_*, что и ввод игрока, а дальше исполняется общий control().
Инфраструктура под это встала прошлым коммитом, здесь — сама логика.
Порт:
- check_can_guard_see_kid (seg003:688) — луч видимости по ряду: стены и
верхи дверей рвут его совсем, loose/чомпер/дыра/неподнятые ворота дают
«вижу, но не пойду». В guards.c (банк 1);
- Opp + loadshad_and_opp (seg006:841) и char_opp_dist (seg006:2135);
- autocontrol_guard_inactive (seg002:710) + move_* (seg002:0706..);
- ветки control(): control_guard_inactive (seg006:2123) и draw_sword
(seg005:945) — соперник уходит сразу в seq_90 en garde;
- pop_guard_tick перестроен по play_guard_frame (seg000:1246): окно
Char/Opp вокруг ИИ, диспетчера и play_seq.
По дороге:
- Kid.alive не выставлялся (= 0 = «мёртв» в семантике оригинала), из-за
чего луч видимости не мог сработать в принципе — ставим -1 в kid_init;
- Kid.room не выставлялась вовсе; условие Kid.room == Guard.room всегда
было ложным. Ставим в enter_room (полная модель Kid.room != drawn_room
у шва по-прежнему впереди);
- pop_tile_at — тайл текущей комнаты наружу из pop_map (луч видимости);
- control_x/y/shift открыты в шину: ИИ заполняет оси как есть, без
flip_control_x (его «вперёд» уже в системе персонажа).
Проверено в MAME (комната 3, страж на tile 17): страж переходит из
стойки 166 в 171 stand_with_sword, sword=2, can_guard_see_kid=2; ввод
игрока не пострадал. Клинок отдельным спрайтом пока не рисуется —
sword.atl содержит только кадры 229..237 (подъём меча Кидом), остальные
строки sword_tbl приедут с боёвкой.
Бюджет (225 кадров, комната 3): 384 168 – 384 636 тактов, 0.893–0.895
кадра, запас 45 364. Прошлый замер 382 584 – 383 064 — шаг стоил ~1 570.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Инфраструктура пункта 2. В оригинале у соперника НЕТ своего диспетчера:
autocontrol_* (seg002) выставляет те же глобалы control_*, что и ввод
игрока, а дальше исполняется тот же control() (seg005:252). Значит перед
портом ИИ надо было привести к этому обе половины:
- control_forward/backward/up/down/shift2 перестали быть static в
pop_ctrl.c — это общая шина синтетического ввода, объявлена в pop_ctrl.h
вместе с POP_CONTROL_*;
- control() переименован в pop_control() и работает с Char, а не с Kid;
- ввод игрока обёрнут в окно Char (loadkid/user_control/savekid), как в
play_frame оригинала.
Ловушка по дороге (ввод отвалился целиком, Kid не двигался): макрос
seqtbl_offset_char вёл на kid_set_seq, который пишет прямо в Kid, а
следом savekid затирал Kid копией Char со старой curr_seq. В оригинале
seqtbl_offset_char работает именно с Char — макрос переведён на
pop_char_set_seq. kid_set_seq остался для вызовов ВНЕ окна (pop_map).
Добавлен pop_savekid_state (Kid = Char без кадра): control() кадр не
трогает, а cur_frame в этот момент принадлежит тому, кто последним крутил
play_seq.
Проверено в MAME: бег и упор в стену работают как прежде.
Бюджет (комната 3, 125 кадров): 382 584..383 064 против 380 292..381 282,
то есть +2 300 тактов на копии окна. 0.891 кадра, запас 46 936.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт 1 плана стражей. play_seq был прибит к Киду, поэтому страж стоял на
захардкоженном кадре 166. Теперь как в оригинале: интерпретатор работает
с АКТИВНЫМ персонажем Char, а вокруг стоят loadkid/savekid и
loadshad/saveshad (порт seg006:809..825).
Почему копия, а не указатель: так в оригинале, и на Z80 это быстрее —
горячий цикл обращается к глобалу абсолютной адресацией, а 16-байтовое
копирование платится один раз на переключение персонажа, тогда как
указатель дал бы индексную адресацию в каждом обращении.
Сопутствующее:
- kid_t и pop_char_t слиты в один pop_char_t (pop_char.h): в оригинале
char_type один на всех, и без этого общий интерпретатор невозможен.
Kid получил поля room/charid/sword/alive — они и так нужны боёвке;
- load_frame выбирает таблицу кадров по Char.charid (у стража своя,
frame_tbl_guard с индексом frame + add_frame − 149, seg006:0293);
- cur_frame разведён на два кэша: страж тикает ПОСЛЕ Кида, и без этого
kid_draw брал бы кадр стража. savekid/saveshad раскладывают кадр по
своему персонажу;
- kid_set_seq пишет ИМЕННО Kid (его зовут pop_ctrl/pop_map вне окна Char,
иначе loadkid затёр бы), для окна Char добавлен pop_char_set_seq —
порт seqtbl_offset_char;
- в pop_map 9 голых play_seq() заменены на pop_kid_play() (load+play+save);
- страж входит в комнату через seq_77_guard_stand_inactive (seg002:0208),
а не через прибитый кадр.
Проверено в MAME: Kid бегает и упирается в стену как прежде, в комнате 12
плиты проваливаются со щебнем (правка задела 9 вызовов play_seq в
физике), страж в комнате 3 рисуется в той же позе, но теперь
curr_seq=0x19A9 и charid=2 — кадр получен прокруткой последовательности,
а не константой.
Бюджет (комната 3, 150 кадров): 380 292..381 282 тактов против
370 140..371 160 до правки, то есть +10 150 (+2.7 %). Основное — не
копии Char, а то, что страж теперь реально крутит интерпретатор каждый
кадр, а раньше стоял замороженным. 0.887 кадра, запас 48 718.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мышь игре не нужна (управление — raw-клавиатура, которую мы и так
забираем у DSS), а её прерывания воруют такты из бюджета, занятого на
86 %. Эффект измерен побочно: при движении мыши на хосте кадры выбивались
до 1.5 кадрового периода, при неподвижной — 225 кадров без превышений.
Записано с тем, что проверить (есть ли в RST 30h выключение, сколько
стоит одно прерывание, восстановление состояния на выходе) и почему не
сейчас: выигрыш только когда игрок двигает мышью, риск оставить систему
без мыши после выхода — заметный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Делитель (torch_tick & 1) занижал скорость пламени вдвое: один логический
кадр = 3 vsync и соответствует игровому тику оригинала, а animate_torch
(seg007:03C1) меняет кадр КАЖДЫЙ тик. Теперь темп задаётся явной
константой TORCH_ANIM_DIV (1 = как в оригинале, 2 = прежнее поведение),
счётчик компилируется только когда он реально нужен.
Побочный эффект важнее визуального: раньше половина кадров делала работу
факелов, половина нет, и бюджет кадра «прыгал». Замер по 100 кадрам
до правки: 349 008..371 262, разброс 22 254 такта (6.2 %). После: по
225 кадрам 370 140..371 160, разброс 1020 тактов (0.27 %) — каждый кадр
стал худшим случаем, и цифре можно верить.
Бюджет сейчас (комната 3, Kid + страж, статика): 0.861..0.863 кадра,
запас 58 840 тактов до 430 000. Кроссбанковых вызовов 19 за кадр
(~654 такта каждый = 12 400, 3.3 % кадра) — столько максимум вернёт
батчинг; профиль вызовов снят breakpoint'ом на ___sdcc_bcall_ehl с
печатью HL/E и раскладкой адресов по .map.
Замечание по методике: мерить надо ПО МНОЖЕСТВУ кадров. Единичные
всплески до 1.5 кадра, которые я сперва принял за проблему движка,
оказались наводкой от прерываний мыши на хосте — при неподвижной мыши
225 кадров подряд без единого превышения.
Проверено, что --w3 (он остался в sprinter-cc) кладёт в W3 только код и
rodata: --dataseg ему не передаётся, глобал --w3 модуля лёг в общий
_DATA — сюрпризов при возврате к резиденту не будет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Резидента --w3 больше нет: отрисовка (pop_bg + pop_gdraw) уехала в БАНК 2,
физика/коллизия (pop_map) — в БАНК 3. Куча W1/W2 1294 -> 6750 Б.
Что это разблокировало. Резидент был тупиком: из банка он недостижим ни
прямо, ни транзитивно, поэтому pop_map (самый крупный модуль, 5.8 КБ) в
банк было не увести — он зовёт mob-отрисовку. Проверено, что банк->банк
РАБОТАЕТ: ___sdcc_bcall_ehl читает страницу окна портом 0xE2 и кладёт её
на СТЕК своего кадра (runtime/bank.s), поэтому вложенность корректна по
построению. Подтверждено в MAME цепочкой W1 -> банк1 -> банк2 -> банк1:
nested=124 after=8, ровно ожидаемое. Значит развязка mob'а (самое
рисковое место, loose-полы) НЕ понадобилась — pop_map зовёт pop_bg
трамплином.
Правила вызовов проверены на сгенерированном asm и записаны в memory
sdcc_banked_call_rules: трамплин выбирает ОБЪЯВЛЕНИЕ (__banked), а не
раскладка — даже внутри одного .c между __banked функциями он есть.
Внутрибанковые функции оставлены непомеченными и зовутся напрямую, в т.ч.
через границу файла (pop_gdraw -> pop_fore_over_char).
sprinter-cc: --dataseg BANKn БОЛЬШЕ НЕ ставится по умолчанию. Раньше вся
писучая память банкового модуля уезжала в страницу банка и снаружи не
читалась (проверено на .map: глобал лёг по 0x0001C000) — грабли на
каждом переносе. Теперь данные банков по умолчанию в общем _DATA (W1/W2,
замаплен всегда), а прежнее поведение — по явному --bank-data.
Замеры (комната 3, Kid + страж; кадр Sprinter в турбо = 430 000 тактов):
до переноса 338 508 (0.79 кадра)
+ pop_bg в банк 2 347 100 (+2.5 %)
+ pop_map в банк 3 371 100 (+9.6 % к исходному, 0.86 кадра)
Плата — трамплины (~654 такта на вызов, ~30 вызовов за кадр). При
пейсинге в 3 кадра это 29 % логического кадра, но запас до ОДНОГО кадра
всего ~59 000 тактов — под звук его надо возвращать (следующий шаг:
батчить кроссбанковые вызовы, начиная с pop_redraw_needed).
Профилирование бордюром включено по умолчанию (make PROF=0 выключает) и
переведено на реально работающие биты: бит 0 (красный) у бордюра Sprinter
ИГНОРИРУЕТСЯ, поэтому различимых состояний четыре и значения обязаны быть
чётными — 0 чёрный (ждём vsync), 2 синий (логика), 4 зелёный (фон),
6 циан (спрайты). Раньше нечётные номера сливались и полосы не читались.
Проверено в MAME: комнаты 1/3 рисуются как прежде, бег и коллизия
работают, в комнате 12 плиты проваливаются со щебнем — то есть цепочка
loose банк3 -> банк2 живая. Полосы бордюра в комнате 3: логика 73
строки, фон 64, спрайты 128, свободно 23.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов. Гипотеза «виноват __banked» ЗАМЕРОМ НЕ
ПОДТВЕРДИЛАСЬ: весь банковый вызов (трамплин + смена страницы + тело
pop_guard_tick + возврат) стоит 654 такта при бюджете кадра 430 000.
Как мерил (выборка PC бесполезна — мост MAME отвечает из фреймового
колбэка, все сэмплы падают в обработчик прерывания): breakpoint'ы MAME с
действием {printf totalcycles; g} на входах фаз главного цикла, разности
соседних меток = стоимость фазы. Плюс профилирование полосами бордюра
(make PROF=1, макрос PROF() в roomtest.c) для быстрого взгляда.
Замер комнаты 3 (Kid + страж), такты, кадр = 430 000:
fore поверх стража 431 964
fore поверх Kid 402 816 -> 78 % всей работы кадра
остальное 241 956
ИТОГО 1 076 736 = 2.5 кадра
Две причины, обе устранены:
1. Fore-слой рисовал ЦЕЛЫЕ тайлы, хотя существует ровно для того, чтобы
вернуть куски поверх спрайта — за его прямоугольником в видеопамяти и
так правильный фон. Введено ОКНО клипа (pop_fore_set_clip): спрайт
сообщает свой итоговый габарит (у Kid — с клинком, брызгами и
обрезкой clip_char), fore-проход режет по нему. Отсев трёхступенчатый:
тайл целиком (tile_in_fclip, до обращения к атласу), кусок по грубому
габариту (до gfx_w0_map — w/h лежат в EMM-странице), и точный клип в
blit_b. Для последнего добавлен libbgi-примитив
gfx_blit_part_noclip — пара к gfx_blit_noclip, но под-прямоугольник.
2. Оставшиеся 341 К после клипа оказались НЕ пикселями: 18 вызовов
pop_prandom за проход, ~10 700 тактов каждый (32-битный LCG:
__mullong ~8 000 + __moduint). Раскладка кладки тайла — чистая
функция (комната, ряд, колонка), то есть константа комнаты, а
wall_pattern пересчитывал её каждый кадр. Теперь кэшируются готовые
РЕШЕНИЯ (что рисовать и с каким смещением), 3 байта на тайл, сброс в
pop_room_draw. Порядок вызовов prandom воспроизведён один в один,
включая то, что значение метки берётся только при сработавшем условии.
Итог того же замера: fore поверх стража 37 464, поверх Kid 46 464,
кадр целиком 334 716 = 0.78 периода (было 2.5). Ускорение 3.2x, сами
fore-проходы — 10x.
Проверка отсутствия регрессии: попиксельная разность скриншотов комнат
1/2/3 до и после — отличаются ТОЛЬКО языки пламени факелов (анимация),
кладка и метки совпадают байт в байт.
Побочно: sprinter-cc научился пробрасывать -DNAME в sdcc.
Память: куча W2 1245 -> 996 Б (кэш кладки 120 Б), резидент W3 12 819 ->
14 512 (свободно 1872 Б — становится тесно), банк 1 236/16384.
ВНИМАНИЕ: make size-check показывает рост 7 программ, но эталон
docs/size_baseline.tsv отстал (последний раз принят в 484b18d, libbgi
менялась в 95c22be/c127a4b/64ce633) — к этой правке рост отношения не
имеет: новый модуль библиотеки в чужие программы не линкуется.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разгрузка W1/W2 перед ИИ стражей (вариант 2 из двух обсуждённых).
1. pop_guard.c разделён по окнам: состояние/логика (Guard, HP, enter,
kill, load_frame) остаются в W1/W2 — их обязан видеть банк guards.c;
ОТРИСОВКА уехала в новый pop_gdraw.c, собираемый как --w3 (резидент).
Правило границы: резидент = только то, что рисует и зовётся
исключительно из главного цикла. Кадр стража стал глобальным
(pop_gframe): заполняет логика, читает резидент.
2. Страж не окклюдировался передними гранями тайлов — рисовался поверх
столба. В оригинале любой Char это запись midtable, а foretable
рисуется после всех midtable (draw_tile_fore, seg008:690), т.е. столб
перекрывает всех одинаково. Футпринт персонажа выделен из
pop_fore_over_kid в char_footprint(), поверх него добавлен
pop_fore_over_char() — слой fore + полоса потолка, без оверлеев поз
виса/полёта/подъёма (у стража их нет; появятся — портируем
redraw_at_char2 общим кодом, а не догадками).
3. Упаковщик стража: тот же off-by-one, что уже ловили у Kid.
load_chtab_from_file(id_chtab_5_guard, 750) даёт images[0] = res751,
а рисование индексирует images[frame.image] — значит image=N это
res(751+N), а не res(750+N). Из-за сдвига frame_166_stand_inactive
рисовался как res767 (выпад) вместо res768 (стойка).
Проверено в MAME: страж в комнатах 3 и 21 стоит в правильной позе;
окклюзия подтверждена патчем Guard.x в живой сессии — при заходе за
столб спрайт корректно срезается его передней гранью.
Память: _CODE 26 149 -> 25 703, куча W2 805 -> 1245 Б, резидент W3
11 656 -> 12 819 (свободно 3565 Б), банк 1 236/16384 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Шаги 1-2 из плана стражей:
1. Спрайты (chtab_5_guard, база 750). Новый упаковщик pop_pack_guard.py:
data/GUARD/res751..784 -> GUARD\g0..g4.atl (5 EMM-страниц, адресация
id>>3 / id&7, как у Kid). Палитра берётся НЕ из PNG, а из res10.bin
(guard_palettes: 7 палитр по 16 цветов, 6-бит) по level.guards_color —
на уровне 1 у обоих стражей color = 2; группа слотов 0x90..0x9F
добавлена в общий kid.pal.
2. Таблица кадров стража у оригинала СВОЯ (frame_tbl_guard, seg006:372,
41 запись) и индексируется как frame + add_frame - 149, где add_frame
= 70 для кадров 102..106. Она дописана в kid_data.bin (3515 -> 3720 Б,
смещение в KID_BIN_GFRAMES_OFF); pop_kid получил pop_kid_data_frame()
— чтение кадра из ЛЮБОЙ таблицы страницы данных.
3. Появление: pop_level_guard() читает guards_tile/dir/color/skill из
уровня, pop_guard_enter() ставит стража по enter_guard (seg002:0112) +
pos_guards (seg003): row из тайла, y = y_land[row+1], x из колонки,
charid = guard, sword сложен, alive = -1, HP = 3. Отрисовка
pop_guard_draw() — та же математика, что kid_draw (load_frame_to_obj +
calc_screen_x_coord), но атлас стража и своя таблица кадров; heal по
странице дабл-буфера, как у Kid.
Интерпретатора последовательностей у стража ПОКА НЕТ: кадр ставится
напрямую (166 = frame_166_stand_inactive, что и даёт seq_77 при входе в
комнату). play_seq для произвольного персонажа + ИИ — следующая фаза.
Проверено в MAME: в комнате 3 страж появляется на своём месте (ряд 1,
кол 7) и рисуется; цвета совпадают с эталонным рендером спрайта в
палитре color=2.
ВНИМАНИЕ по памяти: куча W2 просела до 805 Б (было 2349). Перед ИИ
стражей нужен шаг 5 плана (данные: room_modif 720 Б, dl1/dl2 512 Б,
_kbdraw_down 512 Б) либо вынос кода отрисовки стража в резидент W3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Все .atl лежали в корне диска рядом с exe — с атласами стражей корень
зарос бы окончательно. Теперь ресурсы разложены по каталогам (8.3, как
принято в DSS):
BG\ фон (env0..4, wall, fore, pot)
KID\ персонаж (kid0..27, kid.pal, sword, kid_data.bin)
GUARD\ стражи (появятся здесь)
LEVELS\ уровни (res2001.bin)
make_hdd.sh принимает аргумент вида КАТАЛОГ:файл — создаёт каталог на
образе и кладёт файл туда; без префикса файл идёт в корень. В Makefile
roomtest появилась цель `make hdd`, которая собирает образ с этой
раскладкой (раньше команда набиралась руками на 15 строк).
Проверено в MAME: DSS открывает пути вида KID\kid0.atl — комната
рисуется, Kid и факелы на месте, то есть все атласы, палитра, таблицы
анимации и уровень грузятся из подкаталогов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раскладка (по подтверждённой пробником модели, tests/w3bankgfx):
- roomtest переведён на MEMORY=huge: та же small-раскладка резидента
(CODE в W1, DATA за ним) плюс банки кода в W3;
- guards.c собирается как --bank 1=guards.c — там будет ИИ и боёвка;
- СОСТОЯНИЕ стража живёт в W1/W2 (pop_guard.c): писучие статики
__banked-модуля линкуются в страницу банка и снаружи не читаются, так
что банк — только код;
- поля pop_char_t повторяют char_type оригинала (types.h:302), чтобы порт
seg005/seg006 ложился один в один.
Режим читов (порт cheats_enabled, seg000:111): глобальный флаг pop_cheats,
на время разработки включается в main. Реализован один чит — K (kill
guard, seg000:786): скелета не берёт, живому стражу ставит
guardhp_delta = -guardhp_curr и alive = 0. Обработка по фронту нажатия.
Остальные читы оригинала не портированы.
Проверено в MAME: приложение в huge-раскладке стартует, комната рисуется,
Kid бегает — то есть банкованный pop_guard_tick() зовётся каждый кадр
через трамплин и корректно возвращается; K не роняет приложение (стража
в комнате пока нет). Банк занят на 6 Б из 16384.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сведены дубли, разъехавшиеся по модулям:
- x_bump[20] был в pop_kid (uint8_t!) и в pop_map (int16_t) — теперь одна
таблица int16_t;
- y_land[5] — две копии;
- y_to_row_mod4 — в pop_bg и pop_map;
- 32-битный LCG оригинала (prandom) — в pop_bg и pop_trob; функция теперь
одна, а СИДЫ остались раздельными (у раскладки кладки и у фаз факелов
свои последовательности, смешивать нельзя — иначе поедет рисунок стен).
Экономия по коду скромная (_CODE 24462 -> 24421, W3 11643 -> 11632: часть
выигрыша съели межмодульные вызовы). Главное здесь другое: pop_geom лежит
в W1/W2 и не трогает графику, то есть это тот самый «чистый» слой, который
сможет звать __banked-код стражей (docs/layout_plan_v2.md §4, §5.2).
Проверено в MAME: комната 1 после пересборки отрисована ПОБАЙТОВО так же,
как до правки (0 различающихся пикселей в области комнаты) — значит
последовательности PRNG и геометрия не поехали; Kid бегает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loose-полы, плита-потолок и щебень от приземления больше не зовут pop_bg —
ставят пометки (pop_set_redraw / pop_set_redraw_above), которые разбирает
pop_redraw_needed из главного цикла. Удалены самодельные счётчики
loose_bake/loose_rest/ceil_rest/ceil_bake/land_bake: их роль (вторая
страница дабл-буфера) теперь у счётчика страниц в пометке.
Осталось ОДНО исключение: падающий кусок (mob) — spawn/tick/pos. Это
движущийся ОБЪЕКТ, а не перерисовка тайла, и в оригинале он живёт отдельно
(mobs + draw_moving), поэтому разделение его на логику и отрисовку —
следующая фаза. Из-за него pop_loose_tick остаётся единственной функцией
pop_map, которую нельзя звать из __banked-кода; вся коллизия, физика,
кромки и предметы — то, что понадобится стражам — чисты от графики.
Замер: _CODE 24718 -> 24462, _DATA 4301 -> 4219 (ушли rest-массивы).
Проверено в MAME: комната 12, осторожный шаг на плиту — тряска, падение
плиты, Kid проваливается на ряд 1, дыра и щебень отрисованы; на
замороженном кадре обе страницы дабл-буфера побайтово совпадают в области
изменений (7 строк).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт архитектуры оригинала: логика анимации тайлов НИЧЕГО не рисует, она
ставит флаг (set_redraw_full/set_wipe/redraw_20h/redraw_21h, seg007), а
отрисовка идёт отдельным проходом redraw_needed (seg008:0178). У нас
появился pop_redraw.c/.h: pop_set_redraw(tilepos, вид, страницы) +
pop_set_redraw_above(col, ...) + pop_redraw_needed(), который зовёт
главный цикл в слое фона (до kid_draw).
Отличие от оригинала (наша платформа): счётчик пометки — это ЧИСЛО СТРАНИЦ
дабл-буфера (обычно 2), а вид перерисовки хранится явно (heal+поверх или
запечь фон), потому что у нас у каждой страницы своя ОЗУ-копия фона. В
оригинале вид кодируется тем, в какой из таблиц redraw_frames_* стоит флаг.
pop_trob переведён на пометки: пики, кнопки, дверь уровня. Его самодельные
массивы spike_rest/button_rest/ldoor_rest/rest_pending удалены — их роль
теперь у счётчика страниц в pop_redraw. Прямыми вызовами pop_bg осталось
только пламя факела и пузырёк зелья: это не тайловая перерисовка, а
покадровый оверлей; из-за них pop_process_trobs остаётся единственной
функцией модуля, которую нельзя звать из банка.
Зачем: из __banked-кода резидентная страница W3 недостижима транзитивно
(docs/layout_plan_v2.md §2 R2), поэтому логика, которую будут звать стражи,
не должна вызывать pop_bg.
Проверено в MAME: комната 6 — Kid на кнопке-открывалке, решётка в шве
поднимается; сошёл с кнопки — закрывается; упал в шахту на пики — пики
выдвинулись и отрисованы (кадр 177).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кадры Kid читались из EMM-страницы по НЕВЕРНОМУ адресу для индексов >= 52.
Запись
(const uint8_t *)(0x100) + (uint16_t)i * 5u
SDCC 4.5 собрал так: умножение честно в 16 битах (add hl,hl / add hl,bc),
а затем `ld c,l` + `inc b` — то есть взял только МЛАДШИЙ байт результата и
подставил старший байт константы. При i*5 >= 256 адрес уезжал на -256*k,
и cur_frame наполнялся чужой строкой таблицы: у кадров бега/шага/подъёма
пропадал бит FRAME_NEEDS_FLOOR — Kid «вкручивался» в пол и проваливался
вниз, последовательности кадров не соответствовали seqtbl.
Фикс: адрес считается в uint16_t (i*5 = i + i<<2, без умножения) и
кастуется один раз — сгенерированный код теперь сохраняет старший байт
(ex de,hl / inc d).
Коварство бага: тот же паттерн в pop_level.c (room_fg_ptr/room_bg_ptr,
links) компилируется ПРАВИЛЬНО — проверил все три места по .asm. Записано
в memory sdcc_z80_const_ptr_index_bug.
Проверено в MAME на замороженных кадрах: 10 выборок (кадры 4,10,15,49,50,
54,123) — cur_frame совпадает с таблицей во всех, включая те, что раньше
были испорчены.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--w3 берёт ОДИН файл на флаг, поэтому запись "--w3 pop_trob.c pop_map.c"
означала "W3 = pop_trob", а pop_map всё это время ехал в W1/W2 (в
build-каталоге лежал осиротевший w3_pop_map.rel). Теперь список явный.
pop_trob переведён в W1/W2: в W3 должно оставаться только то, что банк
никогда не позовёт (из __banked резидентная страница W3 не видна ни
напрямую, ни транзитивно — docs/layout_plan_v2.md §2 R2). pop_trob же
стражам понадобится: в оригинале они тоже давят кнопки.
Замер: W3 14376 -> 11643 (свободно 2008 -> 4741 Б), W1/W2 _CODE
21634 -> 24367 (куча 5315 -> 2582 Б). Освободившееся место в W3 —
задел под шаг 3 (loose/потолок из pop_map, чтобы pop_map стал
bank-safe).
Проверено в MAME скриптом: комната рисуется, Kid бежит и тормозит
(кадры 15 -> 10 -> 15), факелы анимируются (152 различающихся пикселя
между соседними кадрами) — то есть pop_trob работает из нового окна.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kid_frames (241*5) и kid_seqtbl (2310) занимали 3.5 КБ в _CODE окна W1/W2
— самого дефицитного ресурса. Теперь они лежат в kid_data.bin (отдельная
EMM-страница), которая маппится в W0 ровно на время play_seq — один
map/unmap за тик, в фазе тика, без конфликта с атласом в W0.
Ключ к переносу — порт load_frame/cur_frame (seg006): оригинал раз за тик
копирует кадр в структуру, и вся коллизия/отрисовка читает ЕЁ, а не
таблицу. У нас так же: kid_cur_dx/kid_cur_flags (их дёргают несколько раз
за кадр из pop_map) и kid_draw читают cur_frame — 5 байт в _DATA.
kid_seq_off (230 Б) оставлен резидентным: его читает kid_set_seq из
pop_ctrl/pop_map, вне страницы.
pop_extract_kid_data.py теперь пишет и kid_data.bin, и урезанный
kid_data.h (тип kframe, размеры, смещения в бинаре, kid_seq_off).
Замер: _CODE 24881 -> 21634 (-3247 Б), куча W2 2076 -> 5315 Б, W3 без
изменений. Проверено в MAME скриптом: стойка -> бег (кадр 8) -> стоп,
перемещение и коллизия у кромки работают, спрайт рисуется.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Новый документ applications/PoP/docs/layout_plan_v2.md по свежему замеру
(коммит 1214785): точные размеры окон/модулей/функций/данных, уточнённая
модель банкинга и пошаговый план.
Главное уточнение против v1: из __banked-кода резидент W3 недостижим — и
транзитивно тоже (bank -> pop_map -> pop_bg сломается). Отсюда целевая
раскладка: W3-резидент = графика, которую зовёт только главный цикл;
W1/W2 = ядро, достижимое отовсюду (включая банки); банки = новая холодная
логика (стражи/боёвка). Проверено по libbgi: скобка _bgi_begin/_bgi_end
сохраняет и возвращает ТЕКУЩУЮ страницу W3, поэтому примитивы libbgi
можно звать и из банка; нельзя лишь открывать скобку из кода, лежащего
в W3.
Крупнейшие цели: kid_data.h (3745 Б таблиц в _CODE) -> EMM-страница с
портом load_frame/cur_frame; вынос loose/потолка из pop_map в W3 (делает
pop_map bank-safe); дедуп геометрии в pop_geom.c; разгрузка _DATA.
Попутная находка: --w3 принимает ОДИН файл на флаг, поэтому в Makefile
"--w3 pop_trob.c pop_map.c" кладёт в W3 только pop_trob, а pop_map едет
в W1/W2 (в build-каталоге остался устаревший w3_pop_map.rel).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- docs/new/ — сводные справочники (архитектура, BIOS, DSS, память,
графика, акселератор, IRQ, порты, ввод, звук, известные баги);
- docs/Original/ — первоисточники, из которых они собраны (BIOS, Estex
DSS, мануалы, описание акселератора), + Форум.doc/.docx в reference;
- libbgi/common/gfx_scroll_h.c — обход бага скролла при ширине >256
(правка автора: шаг банды 255 и продвижение указателей на cw; старый
вариант с 256 оставлен закомментированным с TODO);
- удалён applications/PoP/roomtest/hang_variants.png — рабочая раскладка
из разбора позы виса, в репозитории ей не место.
Большие архивы (docs/extra ~568 МБ, docs/sources с вложенными git-репо
~68 МБ) в коммит НЕ включены — см. обсуждение.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Два дефекта открытой двери:
1. Слева от лестницы оставалась поднявшаяся решётка. Оригинал рисует ВСЕ
куски двери blitters_0_no_transp, а наш упаковщик по умолчанию гонит
пиксель 0 в 0xFF (ключ прозрачности) — марш лестницы 144 переставал
закрашивать створку под собой. Добавил LEVELDOOR_ENV_IDS в
NO_TRANSP_ENV_IDS (тот же приём, что для 43/73/74/96/149).
2. Дверь дрожала через кадр: створка анимируется в back-страницу, и
ПОСЛЕДНИЙ кадр анимации ложился только на одну из двух страниц, вторая
застревала на шаг раньше. Добавлен отложенный редрой (ldoor_rest) —
повтор финального кадра на второй странице, как spike_rest/button_rest.
Проверено в MAME с заморозкой кадра: обе страницы в области двери
побайтово одинаковы, слева от лестницы чистый чёрный фон.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 9: портал на уровень 2 рисовался чёрным проёмом — не был
портирован draw_leveldoor (seg008:1D29). Дверь рисуется при обработке
ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8 px:
99 низ лестницы (всегда),
144 марш лестницы за створкой (когда створка тронулась),
33 слайс створки — повторяется вниз с шагом 4 px до y = ybottom-modif
(modif 0 = закрыто на всю высоту, 43 = открыто, остаётся кромка),
34 верх коробки.
Анимация: animate_leveldoor (seg007:05F1) — type 0..2 открытие (modif++
до 43), type>=3 быстрое закрытие со скоростями {0,5,17,99}. Кнопка
заводит trob через trigger_1 (seg007:0999): дверь открывается ОДИН раз,
при modif != 0 кнопка уже ничего не делает.
Перерисовка створки — pop_leveldoor_redraw: draw_tile правой половины С
ЗАПЕЧКОЙ в ОЗУ-копию (банк TRANSPARENT), иначе kid_heal возвращал бы из
фона закрытую створку. Куски двери непрозрачные, wipe не нужен.
Спрайты 33/34/99/144 не попадали в атлас: сбор идёт прогоном
render_room.py, а он draw_leveldoor не реализует — добавлены явным
набором LEVELDOOR_ENV_IDS (как STUCK_ENV_IDS для нажатой кнопки).
Проверено в MAME (комната 9): закрытая дверь = решётка как в оригинале;
после нажатия кнопки (0,0) створка едет вверх, открывая лестницу.
Вход в дверь (кадры 217..228 + переход на уровень 2) НЕ делался.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. Жёлтая вспышка подъёма меча была ОДНОЙ длинной заливкой. В оригинале
(seg003:0AFC flash_if_hurt / remove_flash_if_hurt) цвет ставится и
СНИМАЕТСЯ в том же кадре — пока идёт flash_time, видно быстрый строб
«жёлтый/чёрный». У нас теперь так же: цвет ставится в кадре и
снимается после ПЕРВОГО gfx_wait_vsync из трёх (≈20 мс жёлтого,
40 мс чёрного — близко к оригинальным 2 тикам таймера).
2. Красной вспышки при потере HP не было вовсе. Порт второй ветки
flash_if_hurt: если flash_time не активен, но в этом кадре hitp_delta<0
— do_flash(color_12_brightred) ровно на кадр. У нас триггер —
pop_kid_hurt (он же рисует «брызги»).
3. Чтобы вспышке было от чего срабатывать, портирован урон падения из
land() (seg005), которого у нас не было: <22 — мягко, <33 — −1 HP и
seq_20 (2 этажа), иначе take_hp(100) + seq_22_crushed (насмерть).
ВНИМАНИЕ: это меняет геймплей — падение с 2 этажей теперь отнимает HP,
с 3+ убивает (как в оригинале).
Проверено в MAME: pop_flash_time тикает 5→3→1→0 (строб), падение с ряда 0
на ряд 2 даёт hitp_curr 3→2 и кадр 109 (medium land).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три расхождения с оригиналом на сцене подъёма меча (комната 15):
1. Лежащий меч не блестел. Порт animate_sword (seg007:0425) +
start_anim_sword (seg007:087C): при входе в комнату тайлу даётся
случайная фаза (prandom & 0x1F), каждый кадр счётчик вниз, на 0 —
новый период 0x28..0x67. Кадр блеска рисуется РОВНО на modif==1
((modif==1)+10 в draw_tile), т.е. одиночная вспышка раз в 40..103
тика. Перерисовка тайла — по смене видимого кадра, схемой кнопки
(текущая страница сразу, вторая через rest-цикл).
2. В кадрах «нашёл меч» клинка не было видно. Порт
add_sword_to_objtable (seg006:1798): клинок — ОТДЕЛЬНЫЙ спрайт
chtab_0 поверх Kid со смещением из sword_tbl. Смещения в ЭКРАННЫХ
пикселях (оригинал применяет их после calc_screen_x_coord), поэтому
берём уже масштабированный obj_x. Из таблицы взяты только строки
35..42 (кадры 229..236) — бой не портирован. Новый атлас sword.atl
(8 спрайтов, 1.6 КБ) + палитра chtab_0 в слотах 0x80..0x8F.
Прямоугольник heal расширяется объединением с клинком, иначе он
оставлял след за габаритом Kid.
3. Не было вспышки фона. do_flash = set_bg_attr(0, color) — оригинал
подменяет НУЛЕВУЮ запись палитры, вспыхивает всё чёрное поле экрана;
proc_get_object ставит flash_color=14, flash_time=8. У нас gfx_pal_set
на обе страницы. ВАЖНО: вызов gfx_pal_set(0,0,0,0,0) пятью литералами
ломает SDCC 4.5 (эмитит невалидный `ld hl, a`) — обёрнуто в функцию с
параметрами.
Попутно: TROBS_MAX 24 -> 30 (как в оригинале) + при входе в комнату из
списка выбрасываются «декоративные» trob ДРУГИХ комнат (факелы/зелья/
меч анимируются только в отрисованной). Без этого отладочный обход всех
24 комнат забивал список, и новые анимации молча не заводились.
Проверено в MAME: блеск (брейк на редрое тайла срабатывает), клинок в
руке виден, фон вспыхивает жёлтым.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 15, тайл (2,2): меч не рисовался вовсе — в draw_tile не было
ветки tiles_22_sword (seg008 draw_tile_anim):
add_midtable(chtab_1, (modifier == 1) + 10, draw_xh, 0, draw_main_y - 3, ...)
Спрайты уже лежали в нашем pot-атласе (chtab_1 пакуется целиком, id 1..23),
так что понадобился только вызов. У нас предмет рисуется статикой в фоне,
а не в midtable: пока меч лежит, он не анимируется, а Kid и так поверх фона.
Подъём — порт цепочки оригинала:
- check_get_item/get_item (seg005:061F/073E) -> pop_get_item_action() в
pop_map (тайлы): стоя НА предмете отступить на тайл назад, подровняться
по кромке и присесть; из приседа над мечом — поднять;
- do_pickup (seg006:1671): тайл -> пол, pop_item_taken = tilepos+1;
roomtest запекает пол на ОБЕИХ страницах (pop_floor_bake) и пишет
per-room override, чтобы меч не воскресал при возврате в комнату;
- триггер — Shift (control_shift2) в стойке и в приседе, как в
control_standing/control_crouched;
- seq_91 pickupsword: опкод SEQ_GET_ITEM с аргументом 1 в play_seq теперь
зовёт pop_proc_get_object (seg006:16CB) -> pop_have_sword = 1.
Статус: pop_have_sword. Боевой режим (стойка с мечом, бой) НЕ делаем и
он тут не нужен: seq_91 сам заканчивается убиранием меча в ножны
(кадры 230..240) и возвратом в обычную стойку — как в оригинале до
встречи со стражем. Питьё зелий не портировано: над зельем Kid только
приседает (get_item возвращает «не обработано»).
Проверено в MAME (комната 15): меч виден на полу, Shift над ним даёт
присед -> кадр 229 «нашёл меч» -> ножны -> стойка; меч с пола исчезает,
pop_have_sword = 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Осторожный шаг к проваливающемуся полу проваливал Kid с первого же шага:
в pop_edge_distance не было ветки оригинала (seg004:067C)
if (tiletype == tiles_11_loose) goto loc_59FB; // CLOSER + до кромки
— плита читалась как обычный пол (EDGE_FLOOR, 11), и весь механизм
«проверки ногой» пролетал мимо. С веткой safe_step (seg005) отрабатывает
три фазы, как в оригинале: (1) укороченный шаг РОВНО до кромки плиты
(seq 29..42 по distance), (2) seq_44 testfoot — щуп ногой, плита трясётся
по SEQ_KNOCK_DOWN, Kid остаётся на месте (Char.repeat), (3) только третье
нажатие — шаг на плиту и падение вместе с ней.
Заодно портированы соседние ветки той же функции: верх двери лицом вправо
(проверяется ДО wall_type) и closer/меч/зелье (кромка, пока есть зазор).
Проверено вручную в MAME: все три фазы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 12, вис и подтягивание на кромке loose-плиты над дырой от
соседней упавшей: плита рисовалась ПОД Кидом — он лез на неё «с
переднего края» вместо проёма. Не хватало двух кусков draw_tile:
1. draw_loose (seg008:0A38) кладёт нижнюю грань плиты (loose_fram_bottom)
В ОБЕ таблицы — backtable И foretable, безусловно. Это единственный
кусок тайла с таким поведением: у обычного пола bottom идёт только в
backtable, а fore_id = 0. Добавлено в fore_tile (наш проход foretable
по тайлам футпринта Кида); ceiling-случай это уже делал отдельно.
2. draw_tile_base (seg008:0A8E) подставляет id: у loose верх плиты берётся
из loose_fram_left, у opener'а без пола слева — 148. В нашем
midtable-оверлее (overlay_mid_tile) стоял голый tile_table.base_id, а у
loose он 0 — верх плиты в оверлей не попадал, и поверх Кида ложилась
только передняя грань. Перенесён draw_tile_base целиком.
Проверено в MAME: кадр виса на кромке целой плиты (frame 89, x=95,
col 2) — плита закрывает Кида, наружу торчат только пальцы, как в SDLPoP.
Голова стоящего Кида поверх падающей НА НЕГО плиты — артефакт САМОГО
оригинала (сверено с SDLPoP v1.24), не чинить: записано в bug_list.md
(раздел «НЕ БАГИ») и в memory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 5, прыжок в решётку: Kid оставался стоять на 6 пикселей выше
пола и без приземления-приседания. Шесть пикселей — это dy(-6) кадра
frame_25_standing_jump_10: удар обрывал standjump ровно между кадрами
25 и 26, парный dy(+6) не выполнялся.
От оригинального bumped() у нас был портирован только хвост (выровнять
X к грани + seq_47_bump). Добавлены недостающие ветки:
- bumped() — исход удара выбирается по тайлу, НА КОТОРОМ персонаж
оказался после отжатия (сквозь стену/верх двери это
соседняя клетка, для ворот/зеркала — сама клетка);
- bumped_floor — прижимает Y к y_land, ветка fall_y>=22 (только отжать
на 5) и выбор сиквенса по кадру: 24/25/40..42/102..106
-> seq_46_hardbump (отскок с приседанием), иначе 47;
- bumped_fall — удар выше пола на 15+ px (беззнаковое сравнение
оригинала) или не над полом: seq_45, в свободном
падении — только гашение fall_x.
Гард «уже в отскоке — не рестартить» (наш, от edge-триггера коллизий)
перенесён внутрь bumped_floor: выравнивание X/Y идёт всегда, рестарта
анимации нет. Проверено в MAME.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комната 1, плита (2,6): её правая грань (env 42, лежит целиком в ячейке 7)
оставалась ПОВЕРХ пола (2,7) и перекрывала его переднюю грань.
Причина: mob_render перерисовывал соседний тайл по m->row — а это
ЛОГИЧЕСКИЙ счётчик «сквозь какой ряд летим», который mob_down_a_row уводит
на ряд вперёд. Для плиты НИЖНЕГО ряда он сразу становится 3, и окклюзия
звала draw_tile(3, col+1) — ряда 3 нет, тайл рисовался за нижним краем
экрана, то есть пол соседа не перерисовывался никогда.
Оригинал берёт ряд ИЗ КООРДИНАТЫ куска, draw_mob (seg007:13E5):
tile_row = y_to_row_mod4(ypos); set_redraw2(tilepos справа)
top_row = y_to_row_mod4(ypos - 18); если отличается — ТОЖЕ пометить
То есть помечаются ДВА тайла справа: под низом куска и под его верхом, пока
кусок висит на границе рядов. Второго у нас не было вовсе — и именно он тут
решающий: при y=194 нижний ряд даёт -1 (полоса у потолка), а верхний — 2,
то есть настоящий пол (2,7).
Проверено в MAME покадрово (bp на pop_loose_mob_spawn + cmd gv; NB: главный
цикл ждёт ТРИ vsync на итерацию, один gv = треть игрового кадра): следов
плиты на полу (2,7) нет, передняя грань цела, потолочная полоса не тронута.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обход графа связей res2001.bin (links @1952) от стартовой комнаты 1: 13, 18
и 24 недостижимы — ссылки наружу у них есть, на них не ссылается никто
(24: L->9, но у 9 R=0). Несимметричные ссылки ровно у этих трёх, у прочих
21 симметрия полная — признак выкинутых из компоновки комнат. В таблицу
обхода добавлена пометка, чтобы не гоняться за призраками: рендер кромки
читает колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
считает, так что странный шов там — свойство данных.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ROOMNAV (одна строка #define в roomtest.c): '+'/'-' — следующая/предыдущая
комната уровня ПО НОМЕРУ (1..24, с обёрткой), Kid ставится на первый пол,
pop_trob_reset() возвращает пики/ворота в исходное. Нужен для обхода всех
24 комнат в поиске багов отрисовки, включая недостижимые обычным путём.
Цена — 505 Б кода W1 (22816 -> 23321), они же вычитаются из кучи (4178 ->
3673 Б); W3 не тронут. Переводить приложение в huge ради этого не стали:
смена модели памяти (банкованный W1 + трамплины) ради полукилобайта —
плохой размен прямо перед прогоном комнат.
Номер комнаты — ПОЛОСКАМИ в верхнем борте (слева десятки, справа единицы),
а не outtextxy: текст тянет системный знакогенератор (_gfx_font_buf, 2 КБ
статики) и сажает кучу до 576 Б. Цвет полосок 0x57, а не WHITE: палитра
игровая, запись 15 в ней ЧЁРНАЯ (kid.pal 0x0F = 0,0,0) — из-за этого
закомментированный ранее лейбл был бы невидим в любом случае.
bug_list.md: раздел TODO (T-1 редрой пик по причине, T-2 idle-skip) +
пустая таблица на 24 комнаты под результаты обхода.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Выдвинутая пика живёт ТОЛЬКО в видео-ОЗУ (банк SPRITE, в запечённый фон не
входит), а kid_heal каждый кадр возвращает под спрайтом Кида печёный фон —
то есть стирает попавшие под него части остриёв. Редрой «только при смене
видимой сигнатуры» их не возвращал: hold держится 15 кадров (modif
0x8F..0x81) с неизменным кадром 5. Итог по дампу VRAM (комната 14, Kid на
(2,7), modif 0x8E): у спрайта 132 стёрто третье остриё (x 245..247), у 138 —
прямое целиком (x 258..260) и низ наклонного, страницы дабл-буфера
расходились на 8 пикселей. SDLPoP: animate_spike (seg007:0353) зовёт
redraw_21h БЕЗУСЛОВНО, вне всяких if — редрой каждый кадр, пока trob жив.
pop_spike_redraw переписан на честный redraw_tile_height (seg007:0218):
heal + ПОЛНЫЙ draw_tile своего тайла и правого соседа. Рисовать только два
спрайта остриёв нельзя — теряется порядок слоёв внутри тайла
(draw_tile_anim_right идёт ПЕРВЫМ, база и fore соседа ложатся поверх), и
остриё лезло на колонну (2,9). draw_tile заодно сам восстанавливает вклад
соседних пик (ветка lcode==2), так что две пики подряд больше не гасят
друг друга.
Пламя факела: heal убран. Оригинал рисует его blitters_0_no_transp (seg008
draw_tile_anim_right), все 9 кадров лежат на общем канвасе 16x18 и бокс
центрирован в ячейке — непрозрачный блит сам полностью накрывает предыдущий
кадр. Кадры пакуются с opaque=True (POT_FLAME_IDS).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кнопка-closer (tiles_6) в нажатом состоянии рисуется как tiles_5_stuck
(get_tile_to_draw seg008:2FE), но её грани — env id 35 (правая) и 36
(нижняя) — не попадали в атлас: pop_pack_bg собирает набор спрайтов
прогоном render_room по статике уровней, а тайла 5 статически в уровнях
нет. Итог — чёрный провал на месте нижней грани. Добавлены явным
набором STUCK_ENV_IDS (как loose/пики/ворота). Заодно OPENER_NOFLOOR_ENV_IDS
= {148}: левая половина кнопки-opener, когда слева пусто (draw_tile_base
seg008:0A8E) — ветка была не реализована ни в pop_bg, ни в render_room.
bake_rest: при ЗАПЕЧКЕ статического фона (запись идёт в ОЗУ-копию
акселератора, из неё восстанавливает heal) get_loose_frame возвращает
кадр ПОКОЯ. Иначе транзиентный кадр тряски соседней плиты консервируется
в копии, а так как запечка идёт двумя кадрами (по одному на страницу
дабл-буфера) — на страницах застывают РАЗНЫЕ кадры, вечное мерцание.
mobs[MOB_MAX] вместо одиночных статиков: две плиты, упавшие подряд,
теряли первый кусок (застывал в воздухе) и оставляли щебень только от
одной. Плюс отложенная допечка прошлого приземления ПЕРЕД разбором
нового и ограничение чёрного бара в pop_loose_bake_empty низом тайла.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kid не мог залезть на тайл поднятой решётки: срывался обратно. can_climb_up
(seg005:09DF) выбирает seq_73 («влез под решётку и съехал») только для
ЗАКРЫТЫХ ворот — условие включает curr_room_modif[curr_tilepos] >> 2 < 6, а у
нас его не было, и seq_73 играл при любой решётке над головой при взгляде
влево.
Та же дыра нашлась в спуске: down_pressed (seg005:482) запрещает climb-down с
тайла ворот лицом влево тоже ТОЛЬКО пока они закрыты
(|| curr_room_modif[curr_tilepos] >> 2 >= 6). Без этого Kid приседал вместо
спуска даже под поднятой решёткой.
Openness обеих проверок берётся общим хелпером gate_modif(col,row) (вынесен из
gate_passable — умеет свою комнату и соседнюю через шов).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три фикса, найденные разбором в MAME.
1. Просвечивание Кида сквозь торец кнопки (подъём/спуск у кромки).
draw_tile_bottom (seg008:570) рисует нижнюю грань блиттером
blitters_0_no_transp: пиксель индекса 0 заливается ЧЁРНЫМ, а не
пропускается. У граней кнопок (env 96/149) и нижних кадров loose (73/74)
последняя строка целиком нулевая — наш упаковщик переводил 0 в 0xFF для
ВСЕХ спрайтов, и она становилась дырой, сквозь которую был виден Kid за
тайлом (на запечённом фоне не видно — там и так чёрное).
pop_pack_bg.py: NO_TRANSP_ENV_IDS пакуется с сохранением индекса 0 как
ЦВЕТА (0x50 = чёрный в палитре env). Ноль тактов в рантайме.
2. Разворот в проёме опущенной решётки выкидывал Кида на другую её сторону.
check_bumped проверял столкновение УРОВНЕМ («край зашёл за грань»), поэтому
разворот на месте внутри тайла-стены менял, какую грань мы меряем, и это
читалось как новое столкновение → Kid.x = char_dx_forward(d) швырял его
сквозь решётку. Оригинал (check_collisions + get_row_collision_data,
seg004:0004) считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ
направления, и зовёт bumped() только на переходе 0 -> 1 (вправо смотрит на
0x0F, влево на 0xF0). Порт этого edge-триггера: флаги от габарита + память
прошлого кадра по колонке, сброс при смене комнаты (в оригинале это
несовпадение prev_coll_room/curr_row_coll_room). Проверено: теперь
разворот даёт уход в комнату 8 (leave_room, взгляд влево, char_x_left<=54)
— то есть туда, где Kid физически и стоит.
3. Снимок шва расширен до ДВУХ колонок с каждой стороны (lcol_fg[6]/
rcol_fg[6]: col9+col8 и col0+col1). Kid, стоящий В шве (curr_col=-1),
смотрит вперёд на колонку -2, где раньше была мнимая стена (get_tile ->
TILE_WALL). Оригинал резолвит любую колонку через find_room_of_tile
(seg006:005D).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спуск Кида с кнопки (room8, кромка (0,6)) рисовался неверно: Кид просвечивал
в щель между кнопкой и ближним столбом, а ближняя рука была срезана до одного
оторванного пикселя. Две независимые причины.
1. Не был портирован clip_char() (seg006:1749) — оригинал перед add_objtable
обрезает спрайт персонажа сверху по y_clip[curr_row+1], когда тайл над
головой стена или пол. Порт: pop_clip_char_top() (pop_map.c, метрики по
set_char_collision) + новый примитив gfx_blit_cols_part() в libbgi (блит
column-major с пропуском skip верхних строк; обрезка сверху бесплатна —
колонка непрерывна в ОЗУ, сдвигается только старт). gfx_blit_cols стал
тонкой обёрткой над ним. kid_heal чистит уже ОБРЕЗАННЫЙ прямоугольник,
иначе стирается кромка пола над срезом.
2. climb_overlay_tile (порт draw_floor_overlay, seg008:1E3A) выбирал ветку по
СЫРОМУ коду тайла, а get_tile_to_draw (seg008:240) подменяет нажатую кнопку
на floor/stuck. Тайл-кнопка не проходил тест floor → уходил в
draw_other_overlay, который кладёт поверх Кида ВЕСЬ тайл вместо узкой
кромки floor_left_overlay[frame-137]. Фикс — tile_code_drawn(): одна
подстановка на все слои (fore_only_tile/overlay_mid_tile перестали её
дублировать, W3 −293 Б).
Проверено покадрово в MAME (шаг gv + снимок): кадры спуска 148..138 чистые,
после приземления на кнопке мусора нет.
Заодно: gfx_heal_noclip() (пара к gfx_blit_noclip; heal 11658 -> 8982 тактов)
и rest_pending в pop_trob — холостой проход по 30 тайлам только когда есть
отложенные редрои пик/кнопок (pop_process_trobs 89-111К -> 70-92К тактов).
START_ROOM временно = 8 (отладка спуска с кнопки), вернуть на 6/старт уровня.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Графика chtab_1 (flame/sword/potion) — новый атлас pop_pot.atl:
- склянка зелья берётся из chtab_1 (seg008 draw_tile_fore: id 12 малая /
13 большая при типах 2..4), а НЕ chtab_6 id 12 — res212 в VDUNGEON нет,
каскад падал на VPALACE и рисовал кусок дворцовой декорации («мусорный
объект» в комнате 5);
- пакуем chtab_1 целиком (id 1..23): пламя, склянки, кадры пузырька;
- MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из
ОБЩЕЙ 16-цветной палитры экрана — она добавлена в 0x30..0x3F, палитра
chtab_1 в 0x40..0x4F; пузырёк пакуется силуэтом цвета 12 (красный),
его маска — цвета 0;
- в env-атлас добавлено основание факела (env 146, seg008:489 — рисует
правый сосед); в статический render_room оно не попадало.
Анимация (seg007 animate_torch/animate_potion + seg000:0B12
anim_tile_modif): при входе в комнату факелам и зельям задаётся случайная
стартовая фаза и заводится trob; факел — get_torch_frame, пламя в ячейке
ПРАВОГО соседа (xh = draw_xh+1, y = draw_main_y−40); зелье —
bubble_next_frame по младшим 3 битам модификатора (старшие 5 = тип, при
загрузке уровня modif <<= 3, seg009). Пламя и пузырёк не запекаются в
фон: heal своей области + кадр поверх. Скорость пламени /2 — наш
логический кадр короче игрового тика оригинала.
Скорость отрисовки (профиль в MAME, кадр = 430 080 тактов):
- замер показал, что цена блита почти НЕ зависит от размера — 13 288
тактов на спрайт 32×3 против 4 617 у линейного спрайтового ядра без
клипа; платим за проход gfx_blit → gfx_blit_part → _gfx_blit_full;
- в libbgi добавлен gfx_blit_noclip() — блит без клипа в ТЕКУЩЕМ банке
(putsprite не годится: навязывает GFX_BANK_SPRITE, фону нужен
TRANSPARENT ради ОЗУ-копии); pop_bg.blit_b уходит на него, когда
спрайт целиком на экране и не нужен g_clip_top → ~2.9× на блит;
- W3-скобку ставит САМА libbgi: из модуля с --w3 её вызывать нельзя —
после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает
(проявлялось белым экраном);
- редрой шва больше не перерисовывает полосу потолка (bar начинается с
POP_YOFF+3) — это удваивало стоимость блока при анимации решётки;
- docs/size_optimization_plan.md §7–§8: замеры, сделанное и запас
(батчинг W3-скобки, решётка одним спрайтом, лишние блиты).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Фича — плита-ПОТОЛОК (loose ряда 2 комнаты сверху, комн.5 (2,5) над комн.6):
- check_press (seg006:1683) портирован целиком: кадры виса 87..99 и подъёма
135..140 «нажимают» тайл, за который Kid держится; кадр 79 — удар снизу
(с проверкой action, как в оригинале); стояние — тайл под ним;
- ряд −1 в pop_map.get_tile = ряд 2 комнаты сверху (порт find_room_of_tile),
состояние тряски/падения — pop_ceil_modif[10]; do_knock(-1) трясёт потолок;
- падение: mob переведён на координаты оригинала (y_loose_land/y_something),
спавнится в ряду −1, по move_loose находит пол → loose_land: debris +
do_knock; heal коридора — по прошлой позиции ДЛЯ КАЖДОЙ страницы;
- урон: check_loose_fall_on_kid + fell_on_your_head (seq_52/seq_22, −1 HP)
и «брызги» draw_hurt_splash (kid image 218);
- колодец наверх: leave_room «вверх» проверяется ПЕРВЫМ (иначе y≈248 при
подтягивании ловил check_leave_below и ронял Kid из уровня);
- персистентность: общий tile-override (щебень внизу, пустота в комнате
сверху) вместо debris-only.
Окклюзия — приведена к оригиналу (найдено трассировкой собранного SDLPoP):
- draw_tile_right/anim_right/draw_loose кладут спрайты в add_backtable
НАПРЯМУЮ, минуя ptr_add_table → правая грань соседа («шахматка» столба 93),
blueline и грани loose/пик соседа ВСЕГДА под персонажем. draw_other_overlay
поверх Kid даёт только: 42 при левом соседе-поле, base, свои пики, bottom
(overlay_mid_tile);
- порядок объектов: тайл Kid (set_objtile_at_char) и порядок обхода
(ряды 2→0, колонки 0→9), внутри тайла — сортировка по y (sort_curr_objs);
то же правило для падающей плиты (pop_loose_mob_draw_over);
- fore-слой рисуется ПОСЛЕ оверлеев (foretable после midtable), иначе тёмный
скос floor_left_overlay ложился поверх колонны;
- fore_tile больше не рисует bottom_id (в оригинале он в backtable) — минус
2..4 блита за кадр;
- полоса потолка поверх Kid = draw_tile_bottom(1)+draw_loose(1)+draw_tile_fore
(arg_0=1 добавляет в foretable), остальное — под ним;
- следы оверлея восстанавливаются на ОБЕИХ страницах (pop_fore_heal).
Прочее:
- control(): при action bumped/in_freefall управление игнорируется целиком
(seg005) — иначе прерванный присед терял dy(1)+dy(1) из medland и Kid
навсегда оставался на 2px выше пола;
- check_bumped: стена ищется по колонке переднего края (а не curr_col) +
гард разворота — Kid больше не проходит сквозь опущенную решётку шва;
- pop_floor_bake для смены floor→debris (bake_empty стирал верх стены ряда
ниже — чёрный прямоугольник);
- в pop_room_redraw_seam_left восстанавливается полоса потолка над решёткой.
Проверено в MAME (bridge); эталон — собранный SDLPoP из applications/PoP/SDLPoP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт Prince of Persia (applications/PoP/roomtest): развитие уровня,
объекты (loose-полы/обломки), переходы между комнатами, фон-упаковка;
планы (room_model/size_optimization), bug_list, pop_trob.
Арт third_party/16x16-RPG-characters.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>