Первый самостоятельный кусок ИИ стража (план, пункт 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>
Шаги 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>
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>
Три расхождения с оригиналом на сцене подъёма меча (комната 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>
Фича — плита-ПОТОЛОК (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>
Проваливающиеся полы в roomtest (порт SDLPoP seg007/seg008), проверено в MAME:
- Падающий кусок (mob): правый край env-42 (w=26, доходит до mob_x+57)
теперь полностью покрыт heal-коридором (MOB_W 56->64) — убран тёмный
хвост-тень на полу 2,7 ПОСЛЕ падения.
- Окклюзия правого куска во ВРЕМЯ падения: после отрисовки плиты
перерисовывается сосед-пол draw_tile(row,col+1) в том же кадре — край
прячется за полом 2,7 (порт redraw_at_cur_mob: set_redraw_full+1).
- Тряска от сотрясения (knock): seqtbl-команды KNOCK_DOWN/UP в play_seq ->
флаг knock -> do_knock(ряд) трясёт loose ряда из покоя (modif=0x80).
KNOCK_DOWN в land-seq (приземление) и runcyc (footstep).
- Фикс «бесконечной тряски» (дабл-буфер): при завершении тряски (0x84->0)
перерисовать покойный кадр плиты на ОБЕИХ страницах (loose_rest, 2 кадра) —
иначе на одной странице застревает дрожащий кадр правой грани (живёт в
тайлах col и col+1) -> мерцание через флип.
Инфраструктура/документация:
- app.mk: цель `make hdd` (упаковка в D: для MCP-моста MAME).
- docs: POP-DAT-FormatSpecifications.pdf/.txt как каноническая спецификация
форматов ресурсов; ссылки в README/MSDOS_RESOURCE_FORMAT.
- README.md/CLAUDE.md для applications/PoP и roomtest (правило «SDLPoP —
источник истины», порядок слоёв, режим отладки freeze 1/2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Порт SDLPoP seg003 redraw_at_char + seg008 set_char_collision: после
kid_draw передний слой (fore_id = foretable-кусок) тайлов ФУТПРИНТА
спрайта Kid перерисовывается ПОВЕРХ него — то, что по изометрии перед
персонажем (передние грани колонн/ворот/большой колонны/щебня). Стены и
факелы имеют fore_id=0 → остаются сзади (в статическом фоне).
- pop_bg: pop_fore_over_kid(obj_x,obj_y,w,h,dir) — считает футпринт
(char_x_left/right, col_from_x, y_to_row_mod4) рядов top..bottom ×
колонок left..right (≤2×2=4 тайла) и рисует fore_id каждого в
GFX_BANK_SPRITE (видео-ОЗУ; kid_heal восстановит из теневого фона,
fore в нём запечён pop_room_draw).
- pop_kid: kid_fp_obj_x/y/width/height — метрики последнего кадра
(obj_x ЛОГИЧЕСКАЯ, до ×8/7) для футпринта.
- roomtest: вызов pop_fore_over_kid после kid_draw.
Проверено в MAME: Kid, идя влево мимо колонны под навесом (row1 col3),
корректно уходит ЗА её переднюю грань (скрывается) и выходит с другой
стороны; задние колонны/факелы остаются сзади.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>