97 Commits

Author SHA1 Message Date
Александр Петров 18d177407b TASKS: зацеп в прыжке — в список на финальную приёмку уровней 1-3
Точки вызова стоят и в обеих ветках check_bumped, поэтому регрессия
проявится не в зацепе, а в обычном ударе о стену с зажатым Shift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:59:59 +03:00
Александр Петров 0cd6b2d737 L3-CHKP, зацеп в прыжке опцией, pop_tune.h; сборка 10 мин -> 1:48
L3-CHKP (чекпойнт уровня 3).  Флаг взводится, когда Кид уходит ВЛЕВО ИЗ
комнаты 7, а do_startpos по нему подменяет старт на комнату 2, тайл (0,6),
лицом влево и снимает loose-плиту (7,0,4).  Тонкость, на которой я сначала
ошибся: level3_set_chkp (seg002:0665) вызван из leave_room ДО
goto_other_room, поэтому `Char.room == 7` — это комната, ИЗ которой уходят,
а не в которую входят.  Поймал пользователь прогоном в SDLPoP: смерть В
комнате 7 вернула его в стартовую 9, а плита осталась цела.  Проверено в
MAME: вход в 7 флаг не ставит, уход влево — ставит; респавн в комнате 2;
после обычной смерти (без чекпойнта) плиты восстанавливаются как раньше.

Зацеп ПРЯМО В ПРЫЖКЕ (check_grab_run_jump, seg006:1228) — портирован за
выключателем POP_ENABLE_JUMP_GRAB.  Это НЕ ваниль: в оригинале зацепиться
можно только в начале падения (кадры 102..105), то есть Shift приходится
жать уже в полёте; у SDLPoP это enable_jump_grab, и работает он лишь при
включённых fixes-and-enhancements.  Три точки вызова как у оригинала:
check_action и обе ветки check_bumped (зацеп за верх стены вместо удара).

pop_tune.h — настраиваемые константы в одном месте (аналог
custom_options_type SDLPoP): чекпойнт, выключатель зацепа и отладочная
крутилка POP_DBG_GATE_HOLD (сколько кадров решётка держится поднятой;
оригинал 5, потолок 30 — таймер связи пятибитный, 31 = «заклинено»).
Задача TUNE-1 в TASKS.md: читать это из ini рядом с exe.

СБОРКА.  --max-allocs-per-node снижен со 100000 (дефолт sprinter-cc) до
3000 (дефолт SDCC) через `make ALLOCS=...`, а pop_trob.c уехал в БАНК 6 —
на 3000 резидент иначе не влезает (замер: конец _HOME 0xBC69 при стеке с
0xBB00).  Итог: сборка с нуля 1:48 вместо >10 минут, куча 2751 Б вместо
2298.  Релизная сборка — make ALLOCS=100000; сравнивать занятость банков
можно только при одинаковом ALLOCS.

Отдельно (вне git, SDLPoP в .gitignore): из референса вычищена вся наша
отладка DBG-GRAB — трасса JMP, GRAB try/probe/fail/OK/skip, автоскриншоты
seg003, печати DBG mob/mid/overlay/kidobj и счётчик dbg_shots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:57:51 +03:00
Александр Петров c6cadd0140 Закрыты BUG-GRAB-1 и BUG-GATEMOD-1; BUG-SPIKE-1 — в низкоприоритетные
Оба фикса подтверждены игрой, записи с разбором корней переехали в
bug_closed.md.  BUG-GATE-PASS-1 остался ждать сценария, но его оговорка
про BUG-GATEMOD-1 обновлена (тот закрыт).  BUG-SPIKE-1 понижен до низкого
приоритета: маловоспроизводим, смертельность пик подтверждена замером.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:50:26 +03:00
Александр Петров 3078886306 BUG-GUARD-DEAF-1 закрыт: страж оборачивается на вернувшегося Кида
Проверено в игре: комната 11 уровня 2, страж выталкивает Кида в 22, Кид
возвращается бегом — страж оборачивается и достаёт меч.  Разбор корня
(is_guard_notice не взводился ни в одном из пяти мест оригинала) переехал
в bug_closed.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:41:20 +03:00
Александр Петров 4424ea20a8 BUG-SPIKE-1: попиксельная подгонка X тоже не воспроизводит — состояние не позиционное
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:55:38 +03:00
Александр Петров eecb00f911 BUG-SPIKE-1: пользователь не воспроизвёл; смертельность подтверждена замером
Запись переведена в «ждёт сценария»: чистый пробег по убранным пикам
убивает (трасса по кадрам в записи), а наблюдавшееся «нет урона» — это
уже выдвинутые пики, безвредные для бегущего и в оригинале.  Открытым
остаётся только визуальное расхождение со скриншотом SDLPoP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:51:15 +03:00
Александр Петров 2b17408609 BUG-SPIKE-1: замер опроверг гипотезу о раннем триггере
Watchpoint на модификатор пики (ур.2 к.6, тайл 13) на чистом пробеге:
  modif=1 — кадр 11 (беговой), x=112, col=2
  modif=2 — кадр 12 (беговой), x=117, col=3   (Кид на тайле, h=2)
  modif=3 — кадр 177 (frame_177_spiked)       (напоролся)

То есть check_spike_below/check_spiked/is_spike_harmful и тайминг
выдвижения верны, «раннего» триггера нет.  Настоящий корень: пики
ЗАЛИПАЮТ выдвинутыми — пока габарит Кида накрывает колонку, каждый кадр
start_anim_spike переставляет отрицательный модификатор обратно в 0x8F.
Выдвинутые пики (h=1) для бегущего безвредны по правилам оригинала, отсюда
обе жалобы: пробег не убивает и острия остаются на экране.

Открытый вопрос сузился до 2–3 пикселей: код start_anim_spike совпадает с
оригиналом дословно, значит на скриншоте SDLPoP Кид стоит чуть левее и
колонку пики не задевает.  План закрытия — в записи.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:41:42 +03:00
Александр Петров dc8b2b7115 Приёмка ур. 2–3: сквозь стену, клин в шве, бар под плитой, глухой страж
Четыре разбора с прогонов пользователя; три бага закрыты, четвёртый
(BUG-SPIKE-1, пики) заведён с замером и гипотезой.

BUG-JUMPWALL-1 (Critical) — недолетевший прыжок проходил СКВОЗЬ стену.
check_collisions держал флаги перекрытия ОДНОГО ряда, а оригинал
(seg004:0004) — трёх, и move_coll_to_prev (seg004:00DF) берёт «прошлые»
флаги из нужного.  Наш prev=3 («уже перекрывал») подавлял бамп ровно на
кадре смены ряда, а в падении ряд меняется почти каждый кадр — переход 0→1
на стене приходился как раз на него.  Порт трёх рядов дословно.
Воспроизведено и закрыто на харнессе (новый набор t_wall: свип по 20
стартовым X, 4 давали проход сквозь кладку); 1723 трассы t_phys НЕ
изменились — правка поведение-сохраняющая.  Живьём подтвердил пользователь.

BUG-GUARD-DEAF-1 (Major) — is_guard_notice не взводился НИГДЕ, поэтому
неактивный страж не оборачивался на Кида за спиной никогда.  Портированы
все пять мест оригинала: опкод SOUND в play_seq (звуки 0..2), bumped_sound,
мягкое/среднее приземление, обрушенная плита, щелчок кнопки.  Ждёт
игровой проверки боем в комнате 11 уровня 2.

BUG-SEAM-WEDGE-1 — клин кладки в пустом (2,0).  Сосед угла снизу-слева
лежит в комнате по диагонали (room_BL); мы безусловно считали его стеной,
оригинал (load_rowbelow, seg008:368) — только когда такой комнаты нет.
Ряд «снизу» стал 11-байтным: [10] = тайл (0,9) диагональной комнаты.

BUG-LOOSE-3 — чёрный бар под упавшей плитой-потолком.  pop_ceil_bake_empty
стирал полосу и восстанавливал только два тайла ряда −1, а в полосу лезет
графика соседа слева и верхушки ряда 0.  Теперь перерисовываются ряды −1 и
0, колонки col−1..col+1.  Проверено попиксельной сверкой с эталонной
перерисовкой: 0 различий.

Плюс карта связности комнат уровней 1–3 (TASKS.md): на ур. 2 недостижимых
нет, на ур. 3 это 23 и 24 — те же односторонние ссылки, что дали 13/18/24
на уровне 1, только комнаты полностью пустые.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:29:37 +03:00
Александр Петров 4737ec323c MEM-BANK5: pop_ctrl.c → банк 5; чит [/] подгонки Кида по X
Три правки едут вместе намеренно: n_banks и --bank обязаны меняться
атомарно, иначе промежуточный коммит — зависание (см. ниже).

MEM-BANK5.  CODE и DATA делят одно 32-КБ пространство W1+W2, поэтому
килобайт кода, уехавший в банк, — это килобайт, доступный данным.
Кандидат выбран не по размеру, а по частоте вызова: диспетчер управления
дёргается раз в кадр на персонажа и горячих банк→банк переходов не
создаёт (в отличие от pop_level, чей pop_level_tile зовётся из банка 2 на
КАЖДЫЙ тайл).

  _CODE   26 780 -> 24 662 Б   (−2 118)
  куча       180 -> 2 298 Б
  банк 5            2 211 / 16 384 (13.5 %)

Шина control_* (8 глобалов) переехала в pop_state.c.  Сегодня она уцелела
бы и в pop_ctrl.c — банки собираются без --bank-data, их писучие данные
остаются в общем _DATA, — но это флаг сборки, а не свойство кода, а шину
трогают уже три банка: 5 пишет с клавиатуры, 1 подаёт синтетический ввод
ИИ (autocontrol_*, seg002), 3 читает через pop_ctrl_shift_held.
Заодно pop_ctrl.c наконец включает собственный заголовок — раньше
объявления жили прямо в нём.

ГРАБЛИ, на которые наступили (стоили дольше самой задачи): n_banks в
roomtest.c захардкожен, и его надо править вместе с числом --bank.  С
n_banks=4 и пятым банком crt0 выделил четыре страницы, _bank_pages[5]
остался нулём, и первый же вызов pop_ctrl_init() через трамплин
отобразил в W3 страницу 0 и прыгнул на 0xC874 в мусор — исполнение
забрело в дисковый код DSS и осталось крутить чтение секторов.  Симптом:
загрузка ресурсов проходит целиком (open=45 — все атласы), комната и Кид
успевают нарисоваться из enter_room, а HP и номер комнаты уже нет, и
kid_tick не вызывается ни разу.  Ровно предупреждение из шапки
runtime/bank.s.  Сверку n_banks с реальным максимальным индексом банка
записал в docs/TODO.md (Auto-banking) — это должно быть ошибкой сборки.

DBG-CHEATS: [ (0x54) и ] (0x5B) двигают Кида на пиксель (seg000:1828),
по фронту нажатия, под pop_cheats.  Нужны потому, что мост MAME теряет
нажатия при быстрой отправке и подогнать Кида в позу скриптом нельзя —
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.

ROOMNAV больше не зовёт pop_trob_reset: reset обнуляет room_seen, то есть
чит ОТМАТЫВАЛ МИР (открытые/закрытые ворота, нажатые кнопки).  Навигация
обязана только телепортировать.

Проверено в MAME: старт уровня 1 рисуется полностью (комната, Кид, HP,
номер), бег вправо и падение на второй ряд отрабатывают, ] даёт x+1 и
[ даёт x−1 по одному нажатию, Shift+→ — осторожный шаг (x 131 -> 142,
колонка 4 -> 5) и при удержании 90 кадров не срывается в бег (x 142 ->
151), то есть pop_ctrl_shift_held работает через границу банк 3 -> банк 5.
Наборы под ucsim: geom 39, grab 53, phys 1723 — все зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:45:15 +03:00
Александр Петров cfc3602375 L2: падение с мечом, разбег-прыжок через 3 тайла, стартовое состояние ворот
BUG-FALL-SWORD-1 — start_fall (seg006:1044) был портирован не целиком:
не хватало трёх веток выбора последовательности и уборки меча в ножны.
Из-за seq_7 с set_fall(1,15) (дрейф 1 px/кадр) Кид с мечом уезжал примерно
на тайл вбок; в оригинале это seq_81_fightfall — падение строго вниз.
Сверено по логу SDLPoP: кадры 102..105 дают x = 155,157,159,160.

BUG-RJUMP-1 — run_jump (seg005:0AA8) не выравнивал Кида по кромке пола
перед толчком, а был заглушкой «полировка K3».  Суммарный dx seq_4 —
62 px при тайле 14, то есть провал ровно в три тайла берётся ТОЛЬКО с
кромки: без выравнивания Кид не перепрыгивал его никогда.  Порт —
pop_run_jump_align() в pop_map (беззнаковое сравнение оригинала = «сдвиг
не попал в [-8,-1]»).  На харнессе: было — толчок с x=165, кадр 44 в
колонке 3 (провал); стало — 5 кадров добега, толчок с x=149, кадр 44 даёт
x=87 col=1 row=1.  Остальные 8 сценариев не изменились.

BUG-GATEMOD-1 — load_alter_mod (seg008:198E) был портирован только для
зелий, поэтому ворота с bg=1 («Open» по спецификации DAT, табл. 8)
стартовали закрытыми.  Добавлены ветки gate (1 -> 188) и loose.  Ветка
wall намеренно НЕ портируется: связи стен наш pop_bg считает по типам
соседей в момент отрисовки.  На уровнях 1-3 таких ворот всего двое
(ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5)) — у остальных bg=2, а 2 и 0
ведут себя одинаково.

Убран временный трассировщик pop_dbg_trace/pop_dbg_draw; pop_dbg_trap()
оставлен как многоразовый инструмент.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:34:26 +03:00
Александр Петров 3d8b81c9f6 build: examples вне регрессной сборки; эталон размеров пересобран
`make all` собирал и examples/, из-за чего цикл «правка libc -> проверка»
упирался в mdview (компилируется минутами) и ничего нового про libc не
показывал.  Регресс ловит разжирение библиотеки, а для этого хватает
мелких tests/ — каждый тянет свой кусок libc и пересобирается за секунды.

- `make all` = tools lib tests; examples собираются явно (`make examples`,
  и как зависимость `make floppy`);
- size_check.py смотрит только tests/*.

Эталон принят заново.  Разбор расхождения, чтобы оно не выглядело
необъяснённым: эталон стоял с 30 июля (0280b05), а libc менялась 1 и 3
августа (b56f2b4 kbd_raw_poll, 1f16e8f fake shift) — _irq_tramp вырос
267 -> 336 Б и не был перебазирован, отсюда +33 Б у всех, кто линкует
трамплин (cbl*, irqtest, rt_test), и +22 у gfx_dbuf (gfx_set_idle_hook в
libbgi из того же KBD-1).  Текущий фикс BUG-KBD-5 вернул 36 Б из этих 69.
Заодно выкинуты мёртвые строки эталона (rpgprof/rpgwalk/scroll/space —
их давно нет в APPS, mdview/mdview2 — теперь вне регресса).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:33:34 +03:00
Александр Петров 030af74631 BUG-KBD-5: зажатый Shift снимался автоповтором стрелки
Симптом: Shift работал в одиночку и НЕ работал вместе со стрелками —
прыжок с зацепом не выходил (BUG-GRAB-1), а осторожный шаг срывался в бег.

Декодеры делали из «fake shift» два вывода, и второй был неверен:
  обёртка E0 F0 12 / E0 12 есть  -> Shift зажат -> взвести бит   — верно;
  расширенный make БЕЗ обёртки   -> Shift отпущен -> снять бит   — НЕТ.

Замер потока байт (MAME, breakpoint на выходе из in a,($18)): клавиатура
pc_kbd ms_naturl обёртку не шлёт вовсе — при зажатом Shift поток на ↑ ровно
`E0 75 E0 75 …`, ни одного F0/12.  А typematic-повторы идут непрерывно,
пока стрелка зажата, значит каждый повтор снимал реально зажатый Shift.
Короткий тап это маскировал: после отпускания стрелки Shift снова
становился последней клавишей, и его собственный автоповтор `12` взводил
бит обратно за ~30 мс.

Фикс: обратный вывод убран, расширенная клавиша о Shift не судит.
Состояние Shift ведут его собственные make/break 12 / F0 12 — они приходят
всегда.  Прямой вывод оставлен (дёшев и верен там, где обёртка есть).
Ушла ставшая ненужной _kbdraw_fakesh; трамплин короче на 36 Б (0x150→0x12C),
что важно — его клавиатурный блок упирается в диапазон jr.

Плата: потерянный при overrun break Shift снять нечем, модификатор может
залипнуть до перенажатия (BUG-KBD-3).  Размен решён как и раньше в
kbd_raw_sync: лучше залипание, чем отвал — сорванный посреди игры Shift
в PoP стоит жизни.

Проверено в MAME чтением _kbdraw_down: Shift+↑+→ зажаты 5 с (автоповтор
идёт) -> LSh остаётся 04; отпускание Shift -> 00.  Зацеп в игре
подтверждён пользователем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:33:15 +03:00
Александр Петров 3fe083331f tests-host: покадровый харнесс сценариев Кида (физика + зацеп)
Проверять физику Кида глазами в MAME дорого и ненадёжно: ошибка почти
всегда не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ через несколько
кадров.  Харнесс гоняет тот же кадр, что и главный цикл
(pop_ctrl_tick -> kid_tick -> pop_phys_tick -> pop_loose_tick), и
сравнивает трассу состояния с эталоном.

- scene.c/.h — раннер: комната + стартовая поза + скрипт ввода -> трасса;
  sc_kid_at_x задаёт точный X (исход часто зависит от фазы внутри тайла).
- stubs.c/.h — libc/libbgi/соседние модули; read() реально отдаёт
  kid_data.bin (иначе kdat_ok=0 и play_seq молчит — трасса замирает).
- t_phys.c — 9 характеризующих сценариев, 1723 сверки (golden/).
- t_grab.c — окно зацепа: существует, достижимо коротким шагом, не
  зависит от рисунка нажатий.
- record_golden.py — снятие эталона по одному сценарию за прогон.
- testkit/host-tests.mk — CODE_LOC настраиваемый, EXTRA_INC/EXTRA_CFLAGS.

Именно харнесс дал доказательство, что физика зацепа у нас верна, и тем
самым перевёл поиск BUG-GRAB-1 на клавиатуру.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:32:55 +03:00
Александр Петров 29d60665e0 docs: уточнение — при падении с мечом рендер не «кривой», а корректный
Отличие от остальных записей раздела «НЕ БАГИ»: там картинка кривая и
совпадает с оригиналом лишь потому, что оригинал сам так рисует (порядок
midtable).  Здесь же спрайт перекрывает кромку ровно настолько, насколько
персонаж за неё зашёл — геометрия и рендер согласованы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:20:27 +03:00
Александр Петров 969f3f1b9a docs: падение при отходе с мечом — НЕ БАГ (сверено с SDLPoP)
Пользователь проверил в SDLPoP v1.24: оригинал падает с той же позиции,
по той же траектории и с тем же видом кадра падения (голова/руки поверх
кромки пола).  Кадры совпадают один в один.

Механика записана с числами: у стоек с мечом weight_x = 13-14 против 3 у
обычной стойки, поэтому при взгляде влево точка веса уезжает на 13 px
вправо от Char.x, и кромку персонаж переступает раньше, чем выглядит.
Замер: x=151 -> dx_weight=164 -> колонка 7 (дыра) вместо 6 (пол).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:19:24 +03:00
Александр Петров 3164065243 Клинок расширяет футпринт перерисовки на колонку (redraw_at_char)
Порт seg003:0430 «If char is holding sword, it makes redraw-area bigger»:
при Char.sword >= sword_2_drawn футпринт расширяется на одну колонку в
сторону взгляда (вправо при dir>=0, влево иначе).  У нас char_footprint
этого не делал, и клинок торчал на колонку дальше области, где
перерисовываются передние грани: меч оставался поверх столба, а его след
— на фоне.

char_footprint получил параметр sword; pop_fore_over_char — тоже (страж
машет мечом ровно так же).  Ветка Кида берёт Kid.sword, ветка стража —
Guard.sword.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:15:34 +03:00
Александр Петров e67117219f BUG-CTRL-FRAME-1: геометрия Кида считалась по кадру стража
cur_frame — один глобал на всех персонажей (как в оригинале), владелец —
тот, кто последним прошёл load_frame.  Последним в кадре тикает страж,
поэтому к моменту control() Кида там лежал кадр СТРАЖА, а через
kid_cur_dx/dy/flags по нему считается вся геометрия управления:
dx_weight -> determine_col -> distance_to_edge_weight -> get_edge_distance
-> выбор ветки в check_jump_up.

Оригинал зовёт load_fram_det_col() (seg006:0144) сразу после
loadkid/loadshad и ДО control() — play_kid_frame (seg000:1211) и
play_guard_frame (seg000:1248).  У нас этого не было.

Замерено брейкпоинтом на pop_jump_up_seq: Kid x=156 col=6 кадр 15
(dx=0 weight_x=3) при кадре стража image17 (dx=-1 weight_x=8) дал
curr_col=7 и distance=2 вместо 6 и 10 — то есть jump_up_plain (вернулось
A=28, пустой прыжок) вместо «шаг назад на x=160 + зацеп».  Предсказание
по кадру стража совпало с намеренным до единицы.

Отсюда же плавающее поведение: кадр стража меняется каждый тик, distance
Кида скакал через порог 6 — то прыжок, то попытка зацепа с неверной X.
И «голова Кида поверх плиты (1,6)» — не баг отрисовки, а следствие позы,
которой в оригинале в этом месте не бывает.

Фикс: pop_load_fram_det_col() (pop_kid.c) + вызовы в pop_ctrl_tick и
pop_guard_tick.  determine_col — только на ветке Кида: у нас он
существует лишь для него (pop_map работает с Kid, а не с Char).

Разбор с числами — bug_closed.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:55:43 +03:00
Александр Петров 5353bdaaec docs: приёмка уровня 2 — первым приоритетом, карта содержимого уровня
Порядок работ по решению пользователя: L2-PASS -> L3-CHOMP/SKEL/CHKP.
Причина техническая — чомперы лягут в банк 2, где живёт отрисовка, и
чинить баги фона поверх свежей механики дороже.

TASKS: запись L2-PASS с картой уровня 2, снятой с res2002.bin —
стражи (5, комнаты 4/7/11/15/24), ловушки и зелья по комнатам, и
декодированные из LINKLOC/LINKMAP цепочки «кнопка -> что открывает»
(в т.ч. кнопка к.9 @1,1, открывающая дверь выхода в к.23).  Плюс
отдельный список того, что сделано именно в L2 и на уровне 1 не
проверялось: большая склянка, меч с начала уровня, выход через дверь,
респавн на своём уровне.

bug_list: заведён раздел «Уровень 2» под список багов отрисовки,
который пользователь подаст отдельно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:46:12 +03:00
Александр Петров 6aefee0cc7 docs: TASKS — цель «уровни 1-3 (подземелье)», разбор что нужно уровню 3
Palace (уровни 4+) отложен решением 2026-08-04.  На доску вынесены три
задачи уровня 3, снятые с данных и SDLPoP, а не с общих соображений:

- L3-CHOMP: 5 чомперов (комнаты 5, 16×3, 22); шаблон как у пик/ворот,
  риск — банк 2 занят на 86.5%.
- L3-SKEL: в данных уровня 3 стражей НЕТ ВООБЩЕ; единственный враг —
  скелет, и он спецсобытие check_skel (seg002:1044), а не страж из
  данных.  Нужен новый атлас (data/SKEL, 29 файлов) — pop_pack_guard.py
  прибит к GUARD/.
- L3-CHKP: чекпойнт (seg002:519 + seg003:141); hitp_beg_lev уже есть.

Плюс инвентарь тайлов по уровням: ур. 3 вводит только chomper(18),
palace-набор (lattice*) начинается с уровня 4 — отсюда и граница скоупа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:38:43 +03:00
Александр Петров e36828ae6e L2: переход между уровнями и игра уровня 2
Порт levels_plan.md §2 (шаг 1) — машинерия смены уровня целиком.

- Номер уровня стал состоянием: pop_current_level (порт current_level),
  pop_next_level больше не флаг, а НОМЕР; главный цикл срабатывает по
  расхождению next != current (порт play_level_2, seg003:0386).
- pop_level_load_num(n): LEVELS\res20NN.bin (fallback a:\), старая
  EMM-страница отпускается только после успешной загрузки новой.
  На диск кладутся все 15 уровней (34 КБ).
- Потабличные различия (data.h:840..848) — таблицы по 16 в pop_level.c:
  tbl_entry_pose, tbl_guard_hp (HP стража ушло из хардкода 3),
  tbl_guard_type (−1 = стражей на уровне нет), tbl_level_type.
- find_start_level_door (seg003:02E6): на уровне 2 стартовый тайл —
  правая половина двери уровня, без modif=43 + add_trob(...,3) Кид
  материализуется внутри глухой створки.  Тип 3 = дверь захлопывается
  за спиной, как в оригинале.
- HP через уровень: hitp_beg_lev (seg003) — рестарт уровня откатывает
  HP к нему, пройденный уровень подтягивает его к hitp_max.  Заодно
  большая склянка add_life (тип зелья 2: +1 к потолку HP до 10) —
  на уровне 2 она есть, комната 20.
- have_sword = level >= 2 (play_level, seg003:106).
- Чит Shift+L — следующий уровень (seg000:698).

Проверено в MAME: уровень 1 → Shift+L → уровень 2 (комната 5, новые для
нас тайлы 8/9 «большая колонна» на месте, дверь захлопнута, HP 3) →
влево в комнату 4 со стражем → Shift+L → уровень 3.  Вход в стартовую
дверь уровня 2 по Up не срабатывает — как и должно (start_room-гард).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:35:56 +03:00
Александр Петров 7ef007757b docs: BUG-SEAM-DRAW-1 закрыт (не воспроизводится), заведён BUG-GATE-PASS-1
BUG-SEAM-DRAW-1: Кид упирается в решётку и встаёт на x=61, curr_col=-1,
room=1 — и в комнате 1 РИСУЕТСЯ, за решёткой.  При x=61 он уже внутри
системы координат комнаты 1 (obj_x = 6), straddle-смещение не требуется.
Остаток теоретический: при x <= 60 спрайт ушёл бы за левую кромку; полное
лекарство — довести S3, но поводов нет, заводить обратно только по живому
наблюдению.

BUG-GATE-PASS-1: однократное наблюдение — Кид стоял НА тайле решётки (0,9)
комнаты 5, дождался закрытия, пошёл вправо и прошёл в комнату 1.  Повторить
не удалось: в том же месте при x=196/col=9 решётка держит штатно.
Плоскость блокировки для колонки 9 — x=205, а «колонка 9» по m7 это
x ∈ [191,205), то есть при curr_col==9 проход невозможен по построению;
значит наблюдался x >= 205, и поведение может оказаться штатным (решётка
закрылась за спиной).  Гипотеза НЕ подтверждена, поэтому баг оставлен
открытым со списком того, что снять в следующий раз, и с двумя запасными
кандидатами на корень.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:20:01 +03:00
Александр Петров f63751edce docs: BUG-LOOSE-2 закрыт — проверено вручную
Падающий кусок теперь привязан к своей комнате и долетает после ухода Кида;
подтверждено живым прогоном.  Автоматикой гонка не воспроизводилась — мост
MAME шлёт нажатия рывками, и «уйти раньше, чем долетит плита» через него не
набиралось; это отмечено в записи вместе с указанием, что кейс стоит первым
в плане host-тестов.

Вторая волна прогона уровня 1 закрыта целиком: BUG-KBD-4, BUG-RESPAWN-2,
BUG-DRAWORDER-1, BUG-LOOSE-2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:04:13 +03:00
Александр Петров a89b8b30c6 docs: BUG-DRAWORDER-1 закрыт; остаток «ноги поверх головы» — в НЕ БАГИ
Сверено в SDLPoP: Кид, стоящий колонкой правее лежащего трупа, и там
рисуется поверх его головы.  Значит порт верен, а артефакт врождённый:
set_objtile_at_char приписывает персонажа РОВНО ОДНОМУ тайлу, ширина
спрайта на выбор тайла не влияет, и всё, что стоит правее, перекрывает
выступающую часть тела.  Механизма «широкий объект в нескольких тайлах» в
оригинале нет — сверено с draw_objtable_items_at_tile, sort_curr_objs и
веткой tile_object_redraw == 0xFF (та про оверлеи пола).

Закрытая запись перечисляет все четыре корня, которые пришлось снять по
очереди: порядок по роли вместо тайла; своя регрессия с общим окном
fore-клипа; колонка трупа из тайла вместо X; непортированная ветка
actions_1_run_jump.  Записано и то, как отличать остаток от этих багов,
и цена «починки» остатка (тайл по центру габарита = отход от эталона).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:02:02 +03:00
Александр Петров 61d4255091 testkit: модульные тесты под ucsim_z80 + планы покрытия
Обвязка для быстрых тестов plain-C логики: секунды вместо прогона в MAME,
без образа диска.  ucsim_z80 идёт в комплекте нашего SDCC — новых
зависимостей нет.

ПОЧЕМУ ПОД Z80, А НЕ ХОСТОВЫМ GCC.  У SDCC z80 int 16 бит, у хоста 32, и
расходится это НЕ в объявлениях, а в выражениях: integer promotion
повышает операнды до int независимо от того, объявлены они как uint8_t
или uint16_t.  Перевод кода на фиксированные типы разницу не убирает —
убирает только исполнение с z80-семантикой.  Побочно проверяется
кодогенерация SDCC и модули с inline-asm, которых хостовая сборка не
видит в принципе.

Устройство: crt0_ucsim.s (SP, зануление, main, halt), tcheck.* (итог в
структуру в ОЗУ), run_ucsim.py (гоняет ucsim, дампит tc_result, печатает
отчёт), host-tests.mk (общие правила).  Вывода через printf нет: тестовый
бинарь линкуется без Sprinter-libc.  Через ucsim-simif не идём — номера
его команд плавают между версиями, halt + dump работают везде.

Наборы лежат РЯДОМ с проверяемым кодом, обвязка общая:
  testkit/t_selftest.c                     — самопроверка (sizeof(int)==2)
  applications/PoP/roomtest/tests-host/    — движок PoP

Первый содержательный набор — t_geom: сверяет рукописный asm-LCG из
pop_geom.c с наивной 32-битной формулой на 128 шагах.  Заявка «бит-в-бит
как в SDLPoP» до сих пор держалась на комментарии.  Тест проверен
мутацией: порча эталонной константы даёт красный.

Планы дальнейшего покрытия:
  docs/host-tests-plan.md                    — libc и libbgi (не начато)
  applications/PoP/docs/host_tests_plan.md   — движок PoP

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:36:16 +03:00
Александр Петров b5d2a81ee3 L1: фиксы прогона уровня 1 — уровень мутабелен, стражи, loose-плиты, порядок
Одиннадцать наблюдений первого прогона свелись к шести корням, четыре
наблюдения второго — ещё к четырём.  Разбор каждого — bug_closed.md.

Первая волна:
- BUG-LVLSTATE-1: уровень стал мутабельным (эталонная копия foretable для
  рестарта, pop_level_set_tile вместо таблицы оверрайдов);
- BUG-RESPAWN-1: рестарт = load_level, тайлы возвращаются из эталона;
- BUG-DEATH-1: смерть от меча доигрывается (порт control_kid, seg006:0CD1);
- BUG-GATE-ANIM-1: ворота в отрисованной комнате перерисовываются
  (POP_RD_GATE, порт draw_trob seg007:01E6);
- BUG-COLL-1: полный порт check_collisions/bumped (seg004) вместо поиска
  стены только в колонке переднего края;
- BUG-STANDUP-1: убран лишний guard в bumped_floor — вставание у стены
  роняло Кида сквозь пол.

Вторая волна:
- BUG-RESPAWN-2: рестарт возвращает и СТРАЖЕЙ (в оригинале play_level на
  каждой итерации делает load_level + pos_guards);
- BUG-LOOSE-2: падающий кусок привязан к своей комнате и долетает после
  ухода Кида (do_mobs крутит mobs[] независимо от drawn_room);
- BUG-DRAWORDER-1: порядок «Кид / страж» задаётся обходом тайлов
  (redraw_needed_tiles: ряды 2,1,0, колонки 0..9), а не ролью персонажа.

По BUG-DRAWORDER-1 понадобилось три захода, и два первых были неполны:
  1) сам порядок — но общее окно fore-клипа осталось стражьим, и Кид
     нарисовался поверх передних столбов (kid_fore_clip_restore);
  2) enter_guard брал curr_col из тайла, а leave_guard пишет туда
     get_tilepos(0,row) — у запомненного ТРУПА колонка была 0 при
     настоящей X.  Теперь колонка выводится из X, как в оригинале;
  3) ветка actions_1_run_jump в set_objtile_at_char оказалась не
     «упрощаемой»: в беге тайл берётся из нижнего ряда и ЛЕВОЙ колонки
     габарита, поэтому бегущий Кид уходит за объекты справа.  Считается
     для обоих персонажей — enter_guard ставит action=1 и стражу.

Проверено в MAME: зелья/меч/плиты переживают выход из комнаты и
восстанавливаются после смерти; кнопка room5 поднимает решётку; падение с
кнопки больше не роняет в комнату 6; убитый страж жив после respawn;
Кид проходит за телом стража.  make size-check — роста нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:35:41 +03:00
Александр Петров 1f16e8fa70 KBD: состояние Shift по «fake shift» — ни залипания, ни отвала
Клавиатура PS/2 обёртывает КАЖДЫЙ расширенный код парой E0 F0 12 / E0 12,
пока реально зажат Shift (замер на железе/MAME: правый шифт обёртывается
своим кодом 0x59).  Это прямое и непрерывное свидетельство состояния
Shift — единственное доступное, потому что опросить PS/2 нельзя, а
typematic повторяет последнюю нажатую клавишу, то есть стрелку.

Оба декодера (_irq_tramp.c, kbd_raw_poll.c) читают обёртку в обе стороны:
обёртка есть -> Shift взвести; расширенный make без обёртки -> Shift
снять.  Бит взводит общий писатель — достаточно обнулить префиксы, и код
уходит в plain-половину карты как make.

Это снимает размен, между крайностями которого мы метались:
  - исключать модификаторы из сброса по overrun -> Shift залипал навсегда
    (BUG-KBD-3);
  - сбрасывать всю карту, как DSS -> Shift сносился каждым overrun'ом, а
    при зажатом Shift тап стрелки это 10 байт в 3-байтовый FIFO, то есть
    overrun почти гарантирован (BUG-KBD-4).
Теперь kbd_raw_sync снова не трогает модификаторы, и это безопасно:
залипание снимается первым же нажатием стрелки.

Раскладка трамплина: клавиатурный блок перевалил за 127 байт, а jp внутри
запрещён (копия в W2).  Префиксные обработчики переехали вплотную к своим
cp, посередине тела стоят ретрансляторы tr_kbd_hub/tr_hub_notkbd/
tr_hub_dss.  В kbd_raw_poll такого ограничения нет — там три jp.

Проверено в MAME: Shift переживает пять тапов подряд; штатное отпускание
снимает; искусственно залипший бит снимается первым тапом.
make size-check — роста нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:16:27 +03:00
Александр Петров ba37bd1133 BUG-DOOR-CLIP: обрезка силуэта правым косяком двери уровня
Симптом (нашёл пользователь сразу после L1-EXIT): при подъёме по лестнице за
дверью уровня силуэт Кида вылезал ПРАВЕЕ правого косяка проёма; по высоте
обрезка была корректна.

Причина — недопортированная половина clip_char (seg006:1231).  Для кадров
двери оригинал ставит ДВА клипа, у нас был только первый:
    obj_clip_top   = leveldoor_ybottom + 1;   // было
    obj_clip_right = leveldoor_right;          // не было
Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
проверяет >= frame_224_exit_stairs_8, то есть 224..228 — портировано по коду.

Fore-слоем это не лечится: створка и косяк уходят в оригинале целиком в
backtable (draw_leveldoor, все add_backtable), рисуются ПОД персонажем и
перекрыть его не могут.  Единственный способ — срезать сам спрайт.

libbgi: gfx_blit_cols_part_w(..., uint8_t maxw) — обрезка СПРАВА у
колоночного блита.  Для column-major это ровно уменьшение числа колонок, то
есть внутри ядра механизм уже был (так же клипается край экрана,
w = _bgi_maxx + 1 - x), наружу не выводился.  Тело блита переехало туда,
gfx_blit_cols_part стал тонкой обёрткой (maxw=0) — тем же приёмом, каким
gfx_blit_cols уже обёрнут вокруг gfx_blit_cols_part.  Работает и при flip:
первые maxw нарисованных колонок всегда ложатся в левую часть футпринта.
make size-check: роста нет.

PoP: pop_leveldoor_right / pop_leveldoor_ybottom (порт одноимённых глобалов)
пишет draw_leveldoor в pop_state — их читает clip_char из другого банка;
pop_clip_char_right() отдаёт границу, kid_draw превращает её в maxw и уводит
эти кадры с noclip-пути на общий.  Прямоугольник heal (kid_lw) сужается тоже
— стираем ровно нарисованное.

Проверено в MAME: pop_leveldoor_right = 176, что есть ровно (draw_xh<<3)+48
для двери комнаты 9; pop_leveldoor_ybottom = 112 у закрытой створки и 69 у
поднятой — сходится с формулой оригинала.  Отрисовку подтвердил пользователь
на живом подъёме.

Заодно: ROOMNAV остаётся включённым осознанно — это наш чит, которого в
оригинале не было, как и S/K/I; позже сведём в общий блок читов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:43:01 +03:00
Александр Петров e86f254b87 L1-START + L1-EXIT: старт по данным уровня и выход через дверь уровня
L1-START.  Старт и оба рестарта (смерть, выпадение из уровня) сведены в
pop_start_level() — порт start_level + do_startpos + set_start_pos (seg003).
Комната/тайл/направление берутся из pop_level_start_*, направление
инвертируется (~start_dir), поза входа — из tbl_entry_pose: у уровня 1 это
падение внутрь (seq_7_fall) плюс нажатие кнопки room5(0,2), то самое, что
захлопывает решётку за спиной.  Жёсткие START_ROOM/COL/ROW убраны.
Проверено в MAME: старт даёт room 1, col 0, падение на row 1 — как по данным.

L1-EXIT.  Ветка двери уровня из up_pressed + go_up_leveldoor (seg005:0482/
0574): тайлы и геометрия — pop_leveldoor_enter() в pop_map, последовательность
seq_70 — в pop_ctrl.  Опкод 0xF1 END_LEVEL в play_seq инкрементит
pop_next_level (порт next_level), главный цикл по нему перезапускает уровень
— ровно та точка, куда levels_plan §2.2 подключит загрузку уровня 2.
Открытость двери проверяется по modifier >= 42 (ветка fix_exit_door), а не по
ванильному leveldoor_open: иначе можно войти в ещё ползущую створку.

Отдельно стоило разбора: go_up_leveldoor сначала писал Char.x/Char.direction,
и оба присваивания молча терялись — окно Char вокруг диспетчера возвращает
назад только curr_seq и sword (pop_savekid_state).  Направление оставалось
«вправо», а все DX в seq_70 отрицательные, поэтому Кид уходил ИЗ проёма
влево (поймано стоп-кадром).  Геометрию персонажа в этом порте меняет
pop_map, пишет в Kid — как pop_down_action и pop_jump_up_seq.

Известный остаток — BUG-DOOR-CLIP в bug_list.md: нет обрезки силуэта правым
косяком проёма (недопортирован obj_clip_right в clip_char); нужен вариант
колоночного блита с ограничением ширины.  ROOMNAV пока оставлен включённым —
он нужен, чтобы попадать в комнату 9 для этой работы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:47:47 +03:00
Александр Петров 1146c57544 CLIP-1: heal Кида и стража — линейным ядром, когда клип не нужен
kid_heal/pop_guard_heal звали gfx_heal всегда, хотя рисуют ровно тот
прямоугольник, который блит в большинстве кадров кладёт noclip-ядром.
Все три места heal (+ heal_off фона) сведены к общему pop_heal_fast.

Замер в MAME, счётчики totalcycles на входах kid_heal и kid_tick (вся
группа heal за кадр), комната 1, Кид стоит:
  клипающее ядро  26 200 тактов/кадр (149 кадров)
  noclip          15 848 тактов/кадр (239 кадров)
−10 352 такта, −39.5 %.  A/B в одном прогоне: вторая половина снята с
пропатченным в памяти условием (jr nz → jr), то есть на той же геометрии.

Размер СУММАРНО −362 Б: _CODE +17, BANK2 −116 (свободно 2708 — это тесный
банк из рисков levels_plan §5), BANK3 −263, BANK4 без изменений.

Грабли по дороге: первым заходом хелпер был static inline в pop_bg.h —
SDCC 4.5 И встраивает тело (181 Б) в каждый вызов, И оставляет копию в
каждом TU, который видит заголовок.  pop_guard_heal раздулся с ~60 до
663 Б, итого +1091 Б в _CODE и +636 Б в банке стража.  Отсюда pop_draw.c:
обычная функция в резиденте W1, из банков это прямой call без трамплина.

pop_room_clip_borders оставлен клипающим осознанно (320 не лезет в 8 бит,
гейт border_dirty редкий) — причина записана в коде.

Проверено визуально: ходьба, прыжок, спуск, позиция за решёткой шва
(straddle — там работает клипающий фолбэк) — артефактов нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:53:30 +03:00
Александр Петров d552cbaca9 docs: bug_list — только открытые баги; закрытые → bug_closed.md
Три Critical'а (BUG-1 провал на row 1 при боковом переходе, BUG-2 ping-pong
при возврате, BUG-3 окклюзия climb-up на кнопке) висели непроверенными с
2026-07-21.  Прогнал в MAME:

- BUG-1 не воспроизводится: room6 → кнопка (0,2) → открытая решётка →
  переход влево даёт room8, y=55, curr_row=0.  Заодно снят и сам диагноз
  записи — репроекция Y при БОКОВОМ переходе не нужна: goto_other_room
  (seg002.c:390) меняет только x, наш check_leave делает то же.
- BUG-2 не воспроизводится: шов room2↔room3, четыре пересечения с
  разворотом сразу после входа — комната меняется ровно раз на пересечение.
- BUG-3 закрыт фиксом tile_code_drawn от 2026-07-28 (это дубль уже
  записанного «спуск с кнопки»); оговорка про непереснятый подъём — в
  bug_closed.md.

bug_list.md теперь только открытое (BUG-CEIL-1/2/3, BUG-OCCL-1, T-1, T-2,
таблица обхода 24 комнат) + индекс с якорями.  bug_closed.md — закрытое
вместе с разбором корней (odd-pixel char_x, подстановка тайла кнопки, баг
кодогенератора SDCC), он и есть главная ценность архива.

TASKS.md: кросслинки на открытые баги в шапке, в L1-TRIAGE, L1-PASS и
«Отложено».  Указатели в CLAUDE.md/README/room_model_plan/layout_plan_v2
переведены на нужный из двух файлов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:57:33 +03:00
Александр Петров 6b4a3b6b41 docs: итог KBD-1 — что лечит плотный опрос и что осталось
Ручная проверка пользователем: стало значительно лучше, но редкие пропуски
стрелок при зажатом Shift всё же ощущаются.  Счётчики на 35 нажатиях подряд
потерь не показали, то есть остаточная частота заметно ниже прежних ~15 %.
Задача отложена до финальной полировки программы (решение пользователя) —
для работы клавиатура пригодна.

Записано, где именно осталась дыра, чтобы не начинать с нуля: idle-хук
покрывает простой (~2/3 кадра), а в занятой трети DI-окно одного
accel-прохода доходит до ~650 мкс при допуске FIFO ~300 мкс — пачка байт,
целиком попавшая в такое окно, ещё может потерять байт.  Порядок действий
на возврат: вызовы между блитами занятой фазы, замер тем же счётным методом
от 50 нажатий, и только потом рычаги вне нашего кода (Scan Code Set 3 через
BIOS $EA — в MAME непроверяемо; общий m_irq_off_timer в драйвере).

Заодно сняты оговорки «плотный опрос ещё не подтверждён замером» в
kbd_raw.h и libc-reference.md — теперь там штатный рецепт через
gfx_set_idle_hook.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:15:21 +03:00
Александр Петров 4b498d171b libbgi: idle-хук в ожидании кадра; им лечится потеря нажатий с Shift
Причина потерь (замеры — applications/PoP/roomtest/TASKS.md, KBD-1): при
зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», нажатие
стрелки становится 5 байтами вместо 2, а импульс запроса прерывания здесь
теряется примерно в 44 % случаев — трёхбайтовый FIFO SIO переполняется, и
байт пропадает ДО чтения порта.  Лечится только плотным вычерпыванием: раз
в ~0.5 мс.  Столько времени есть даром — при пейсинге «3 растровых кадра на
логический тик» процессор проводит ~42 мс из 60 в gfx_wait_vsync, крутя
опрос луча и больше ничего не делая.

- gfx_set_idle_hook(fn) — что вызывать, пока gfx_wait_vsync ждёт луч.
  Состояние в отдельном data-модуле (_gfx_idle_state.c), чтобы не тянуть
  сеттер в программы, которые хук не ставят.
- Лучевой цикл зовёт хук в обеих фазах.  BC (счётчик таймаута)
  сохраняется, косвенный вызов — push адреса возврата + jp (hl), так как
  `call (hl)` в Z80 нет; без хука это ret по нулевому указателю, порядка
  двух десятков тактов в цикле, который и так сжигает время.
- Путь FPS-делителя не затронут: там ожидание через HALT.
- roomtest вешает на хук kbd_raw_poll.

Проверка в MAME счётчиками (брейкпоинты с { b@ADDR = b@ADDR+1 ; g } на
чтении порта 0x18 и на установке make-бита): 35 нажатий Shift+Home → 35
make, ноль потерь; до фикса было 9 из 10.  Боевой сценарий: четыре Shift+→
подряд дали четыре осторожных шага (Kid.x 114 -> 147).  _CODE +170 Б,
кадровый бюджет не затронут.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:05:24 +03:00
Александр Петров b56f2b4582 libc/kbd: kbd_raw_poll + замер потери нажатий при зажатом Shift (KBD-1)
Симптом: при удерживаемом Shift часть нажатий стрелок не отрабатывает
(~15 % по наблюдению пользователя), без Shift потерь нет.

Переведено в числа: нажимается Home — тоже расширенная клавиша (тот же
E0-префикс и тот же «fake shift»), но игрой игнорируется, поэтому рельеф
комнаты на результат не влияет.  Счётчики — брейкпоинты MAME с действием
{ b@ADDR = b@ADDR+1 ; g } на входе клавиатурной ветки трамплина, на чтении
порта 0x18 и на установке make-бита.

Что измерено (10 нажатий Shift+Home, дошло make):
  игра идёт, опрос ВКЛ   9/10     игра идёт, опрос ВЫКЛ  9/10
  игра ЗАМОРОЖЕНА (блитов нет вообще, длинных DI нет)  8/10

Обе исходные гипотезы отпали:
- длина наших DI-окон ни при чём (в замороженном кадре потерь больше);
- снятие di в accel-ядрах libbgi УРОНИЛО машину — режим «акселератор при
  EI» из docs/new/06-accel.md §6.6 в этой прошивке недоступен.

Байт теряется НИЖЕ нашего кода: на 49 прочитанных байт пришлось только 28
входов в клавиатурную ветку, то есть ~44 % импульсов запроса прерывания не
обслуживается и трёхбайтовый FIFO SIO переполняется.

Потолок приёма измерен отдельной программой tests/kbdpoll (ничего, кроме
kbd_raw_poll в цикле): 25 нажатий -> 25 make, 250 байт из 250, ноль потерь.
Значит опрос лечит полностью, вопрос только в плотности: нужно раз в
~0.5 мс, а шесть вызовов за 60-мс кадр давали раз в 10 мс.

Поэтому вызовы из roomtest.c УБРАНЫ (они стояли там, где прерывания и так
разрешены, и дублировали трамплин — 9/10 с ними и без).  Сама функция
kbd_raw_poll оставлена в libc: она корректна и нужна как основа плотного
опроса.  В заголовке и в libc-reference — честная оговорка, чтобы её не
ставили в игровой цикл «на всякий случай» без замера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:33:16 +03:00
Александр Петров 774b1cc7c4 docs(PoP): документация к актуальному статусу + план следующих уровней
Документы отстали от кода: PORT_PLAN писал «PoC не начат», хотя играется
весь уровень 1, а четыре плана были исполнены целиком.

- PORT_PLAN: таблица статусов по разделам; фазы 0-3 сделаны, 4-6 нет;
  риски §8 п.1/п.3 закрыты, п.2 переформулирован под реальный движок
  (спрайтовый движок для персонажей не используется, лимит «21 спрайт»
  неприменим), п.4 — найдено расхождение таймингов: оригинал считает
  логический кадр за 5 тиков при BASE_FPS=60 (83.3 мс, в бою 100 мс), а мы
  ждём три vsync (60 мс) — игра идёт примерно на 39 % быстрее эталона.
- levels_plan.md — новый: машинерия перехода между уровнями, второй
  тайлсет (palace), потабличные различия и читы SDLPoP, которые окупаются
  сразу.  Инвентарь тайлов снят прямо с res200N.bin: уровень 2 не требует
  ни одного нового ассета и ни одной новой механики.
- roomtest/TASKS.md — новый: доска текущих задач с критериями готовности.
- Удалены как исполненные и перекрытые кодом: clip_char_plan,
  double_buffer_plan, loose_floors_plan, size_optimization_plan.  Его §8
  (замеры скорости отрисовки) не был перекрыт — перенесён в
  layout_plan_v2 §9, чтобы не потерять цифры.
- KID_PLAN / gates_spikes_plan — шапки «реализовано, оставлено
  справочником»; room_model_plan — «S1 сделан, остальное не срочно».
- docs/README.md стал индексом с отметками актуальности.
- ideas_backlog: зелье переворота экрана — оригинал переворачивает готовый
  буфер построчно, спрайты не трогает; по данным уровней тип 4 встречается
  только на уровне 9, до него механика не нужна.
- examples/scroll: ссылка на удалённый план вела к неверному факту
  «теневая копия одна — общая»; заменено на подтверждённое «у каждой
  страницы своя».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:32:48 +03:00
snark13 2f3e854854 PoP roomtest: отрисовка стража — в собственный банк (банк 2 упёрся в потолок)
Банк 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>
2026-07-30 22:22:27 +03:00
snark13 75a51fb1db PoP roomtest: выпивание зелья (уровень 1 — склянка здоровья)
Каркас предметов уже был (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>
2026-07-30 22:09:36 +03:00
snark13 a32b66700f toolchain: make_hdd.sh — закрывать mtools ОБА канала запроса, не один
Прошлая правка убрала только управляющий терминал (os.setsid), и mmd
переключился на stdin: lsof показал fd 0 = /dev/ttys002, процесс снова спал,
теперь уже после отметки «копирование файлов».

Каналов, откуда mtools может ждать ответ, два — /dev/tty и stdin — и
закрывать надо оба.  Обёртка mt() теперь и создаёт новую сессию, и подаёт
stdin из /dev/null.

Проверено: с stdin=/dev/null mmd на свежем образе отрабатывает с кодом 0, а
на уже существующем каталоге честно возвращает 1 и ничего не спрашивает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:01:39 +03:00
snark13 59e51f7e83 toolchain: make_hdd.sh не виснет при запуске из терминала
Симптом: сборка образа молча вставала навсегда сразу после эхо-строки
команды.  Появилось не «само» — ровно тогда, когда образ разложили по
подкаталогам (BG/KID/GUARD/LEVELS) и в скрипте появился mmd.

Диагноз по артефакту, а не по догадке: зависший процесс — `mmd z:/BG`,
и lsof показал fd 0 = /dev/null, fd 4 = /dev/tty.  То есть mtools (собран
с enable-raw-term) для интерактивного вопроса открывает УПРАВЛЯЮЩИЙ
ТЕРМИНАЛ напрямую, в обход stdin — поэтому ни `< /dev/null`, ни
перенаправления stdio не помогают.  А `2>/dev/null` на mmd прятал сам
вопрос, из-за чего это выглядело как зависание на пустом месте.
Из НЕинтерактивного запуска (CI, фоновая задача) терминала нет, вопрос не
задаётся, и баг не воспроизводится — потому и жил незамеченным.

Лечение: все вызовы mtools идут через обёртку mt(), которая запускает их в
НОВОЙ СЕССИИ (os.setsid + exec питоном; setsid(1) в macOS нет).  Без
управляющего терминала открывать /dev/tty нечего, и mtools выбирает
неинтерактивный путь.

Заодно добавлены отметки этапов («разметка и формат», «копирование
файлов», «конвертация RAW -> CHD»): если что-то встанет снова, будет сразу
видно где, а не после последнего аргумента команды.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:57:39 +03:00
snark13 b22cee3456 PoP roomtest: убитый страж остаётся мёртвым; меч только по подбору или читу S
Смерть стража теперь персистентна между входами в комнату — по механизму
оригинала, а не отдельной таблицей «убит/не убит».  В 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>
2026-07-30 21:33:21 +03:00
snark13 2e90eaf7d7 PoP roomtest: окно Char больше не затирает правки pop_map (спуск с уступа)
Регрессия от окна 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>
2026-07-30 18:51:49 +03:00
snark13 3983fa4513 PoP roomtest: HP-учёт, индикаторы HP, чит бессмертия; фикс кэша кадра
Боёвка (порт 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>
2026-07-30 18:43:39 +03:00
snark13 d014a3f577 PoP roomtest: ИИ стража — подход к Киду и боевые ветки диспетчера
Пункт 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>
2026-07-30 17:24:53 +03:00
snark13 4f7d9c0596 PoP docs: запасные PRNG (LFSR/LCG, xorshift(7,9,8)) + оценка потолка выигрыша
Тексты обеих 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>
2026-07-30 17:10:35 +03:00
snark13 37fc572cc3 PoP roomtest: LCG оригинала на ассемблере — точность без потери скорости
Возврат к БИТ-В-БИТ генератору оригинала по умолчанию (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>
2026-07-30 17:05:13 +03:00
snark13 99b430f2ed PoP/libbgi: убрана 32-бит арифметика, noclip для column-major, быстрый PRNG
Ревью на 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>
2026-07-30 16:48:37 +03:00
snark13 d438a1d3da PoP roomtest: клинок в атласе целиком + раздельный heal накладных спрайтов
Меч в оригинале ОДИН на всех: и Кид, и страж рисуют клинок из 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>
2026-07-30 11:40:08 +03:00
snark13 79ea473910 PoP roomtest: страж замечает Кида и достаёт меч (ИИ, шаг 1)
Первый самостоятельный кусок ИИ стража (план, пункт 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>
2026-07-30 11:09:12 +03:00
snark13 cf06896dbd PoP roomtest: окно Char вокруг control() + общая шина ввода — база под ИИ стража
Инфраструктура пункта 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>
2026-07-30 10:32:15 +03:00
snark13 8bbc6b4d07 PoP roomtest: интерпретатор последовательностей стал общим (Char) — стражи
Пункт 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>
2026-07-30 10:11:10 +03:00
snark13 565ba98852 PoP docs: бэклог идей — начат с «отключать мышь на время игры»
Мышь игре не нужна (управление — raw-клавиатура, которую мы и так
забираем у DSS), а её прерывания воруют такты из бюджета, занятого на
86 %.  Эффект измерен побочно: при движении мыши на хосте кадры выбивались
до 1.5 кадрового периода, при неподвижной — 225 кадров без превышений.

Записано с тем, что проверить (есть ли в RST 30h выключение, сколько
стоит одно прерывание, восстановление состояния на выходе) и почему не
сейчас: выигрыш только когда игрок двигает мышью, риск оставить систему
без мыши после выхода — заметный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:58:40 +03:00
snark13 ac9871c58c PoP roomtest: факел анимируется каждый логический кадр (TORCH_ANIM_DIV)
Делитель (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>
2026-07-30 09:57:26 +03:00
snark13 1e6c377edc PoP roomtest: pop_bg и pop_map в банки; --dataseg BANKn стал опцией
Резидента --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>
2026-07-30 09:31:58 +03:00
snark13 0280b05933 libc/kbd: held-карта клавиш в биты (512 -> 64 Б); эталон размеров принят
Разгрузка W1/W2 под будущий ИИ стражей: _kbdraw_down был БАЙТОМ на
скан-код (512 Б в _DATA при 32-килобайтной раскладке).  Теперь бит на
код: код>>3 = байт, код&7 = бит, расширенные (префикс 0xE0) — смещение
+32 байта вместо +256.

Трамплин прерывания строит маску СДВИГОМ, а не таблицей: таблица
потребовала бы `ld hl,#метка` внутри трамплина, а он копируется в W2
побайтно и обязан быть без абсолютных само-ссылок (см. его шапку).
Маска строится в BC, поэтому в клавиатурной ветке добавлен push/pop bc.
Трамплин вырос 244 -> 267 Б, буфер копии поднят 320 -> 336 (запас 69 Б).

Проверено в MAME на roomtest, все три класса клавиш:
  - обычные: '=' (обход комнат) и 'K' (чит-убийство стража — читал
    guardhp_curr/delta: 3/0 -> 0/-3);
  - расширенные (E0): стрелка вправо — Kid добежал до края комнаты;
  - модификаторы: удержание Shift ставит бит 2 байта 2 карты
    (скан-код 0x12), отпускание снимает.

Скорость: кадр 334 716 -> 338 508 тактов (+1.1 %) на битовой арифметике
в kbd_raw_down (~15 вызовов за кадр); при бюджете 430 000 это 0.79
периода вместо 0.78 — регрессии нет.

Итог по roomtest: данные 4422 -> 4022 Б, куча W2 996 -> 1294 Б.

Эталон размеров принят заново (make size-baseline): _CODE десяти
программ вырос на 14-23 Б — это код битовой арифметики в трамплине и
kbd_raw_down, обмен на -448 Б данных, которые size_check не считает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 00:50:37 +03:00
snark13 f2093e0d89 PoP roomtest: fore-слой — окно клипа, кэш кладки; кадр 2.5 -> 0.78 периода
Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов.  Гипотеза «виноват __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>
2026-07-30 00:19:32 +03:00
snark13 af5f0a4638 PoP roomtest: отрисовка стража в резидент W3 + fore-окклюзия + off-by-one спрайта
Разгрузка 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>
2026-07-29 23:14:21 +03:00
snark13 3dbad6120c PoP roomtest: страж — спрайты, таблица кадров и появление в комнате
Шаги 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>
2026-07-29 21:57:38 +03:00
snark13 1dc89b0f26 PoP roomtest: ресурсы по каталогам образа (BG/KID/LEVELS), цель make hdd
Все .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>
2026-07-29 21:42:16 +03:00
snark13 e1ee447b7f PoP roomtest: каркас стражей в БАНКЕ + режим читов (K — убить стража)
Раскладка (по подтверждённой пробником модели, 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>
2026-07-29 21:31:11 +03:00
snark13 18f5115e0d docs(PoP): план v2 — результат пробника банка (модель подтверждена)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:19:00 +03:00
snark13 b3bf2cca3d tests/w3bankgfx: пробник модели «huge + резидент W3 + банк W3 + графика»
Проверяет то, на чём стоит план раскладки PoP (layout_plan_v2.md §2):

R3  резидент/HOME -> __banked через трамплин работает;
R4  примитивы libbgi можно звать ИЗ БАНКА: _bgi_begin запоминает текущую
    страницу W3 (порт 0xE2), _bgi_end её возвращает — банк переживает
    рисование и продолжает исполняться;
    то же верно для функции W1/W2, вызванной из банка: она рисует, а в W3
    остаётся страница БАНКА, не резидента.

Результат в MAME: все пять полос на месте, вердикт ЗЕЛЁНЫЙ.  Замеры:
страница банка 0xF0 до рисования, после прямого блита и после возврата из
W1/W2-функции — та же 0xF0; резидент 0xF3; банк дожил до конца и вернул
корректное значение.

Два побочных вывода, важных для стражей:
1. Писучие статики __banked-модуля линкуются В СТРАНИЦУ БАНКА (адрес
   0x1C000+), снаружи их не прочитать — состояние банка держать в W1/W2.
2. Инлайновый `in a,(0xE2)` посреди тела функции затирает A, куда SDCC уже
   положил параметр (в первой версии пробника цвет заливки становился
   номером страницы, и «резидент не рисовал»).  Читать порт отдельной
   __naked-функцией.

Имя exe — 8.3 (w3bgfx.exe): DSS длинных имён не понимает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:18:27 +03:00
snark13 3b2dd8bfcc docs(PoP): план v2 — статус выполнения шагов 1..4 и найденная ловушка SDCC
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:08:47 +03:00
snark13 ecf5ecfc14 PoP roomtest (план v2, шаг 2): общая геометрия в pop_geom
Сведены дубли, разъехавшиеся по модулям:
- 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>
2026-07-29 21:05:24 +03:00
snark13 4cffbc9aa4 PoP roomtest (план v2, фаза 1b): pop_map переведён на пометки перерисовки
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>
2026-07-29 20:44:56 +03:00
snark13 24bb724c22 PoP roomtest (план v2, фаза 1a): пометки перерисовки вместо прямых блитов
Порт архитектуры оригинала: логика анимации тайлов НИЧЕГО не рисует, она
ставит флаг (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>
2026-07-29 20:37:03 +03:00
snark13 eef6eebd8c PoP roomtest: фикс чтения таблицы кадров — баг кодогенерации SDCC
Кадры 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>
2026-07-29 20:24:03 +03:00
snark13 3de8c7500c PoP roomtest (план v2, шаг 4): в W3-резиденте остаётся только pop_bg
--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>
2026-07-29 19:30:57 +03:00
snark13 8e33cd07bc PoP roomtest (план v2, шаг 1): таблицы анимации Kid -> EMM-страница
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>
2026-07-29 19:15:16 +03:00
snark13 a5773ab654 docs(PoP): план v2 — размер кода и раскладка по окнам/банкам/страницам
Новый документ 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>
2026-07-29 18:55:36 +03:00
snark13 68b8fe5851 .gitignore: не версионировать docs/extra и docs/sources
docs/extra — ~570 МБ архивов чужих исходников (525 МБ из них — четыре
почти одинаковых zip'а bad_apple); docs/sources — клоны чужих
репозиториев со своими .git внутри, которые при обычном add стали бы
битыми gitlink-ссылками (без .gitmodules клон их не подтянет).

Материалы остаются на диске, но в историю не попадают: раздувание репо
необратимо без перезаписи истории.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:28:19 +03:00
snark13 64ce6339eb docs: справочники по железу Sprinter + правка gfx_scroll_h
- 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>
2026-07-29 18:26:31 +03:00
snark13 1214785a56 PoP roomtest: дверь уровня — непрозрачные куски + финальный кадр на 2-ю страницу
Два дефекта открытой двери:

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>
2026-07-29 18:18:01 +03:00
snark13 63a25530f5 PoP roomtest: дверь уровня — створка, лестница и анимация открытия
Комната 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>
2026-07-29 18:05:57 +03:00
snark13 fb495ace42 PoP roomtest: вспышка фона стробом + красная вспышка урона (+ урон падения)
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>
2026-07-29 17:40:52 +03:00
snark13 e1846369ad PoP roomtest: блеск лежащего меча, клинок в руке, вспышка фона
Три расхождения с оригиналом на сцене подъёма меча (комната 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>
2026-07-29 17:22:22 +03:00
snark13 1c91c5f92a PoP roomtest: меч — отрисовка на полу, подъём по Shift, статус have_sword
Комната 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>
2026-07-29 16:46:09 +03:00
snark13 6769b6a01b PoP roomtest: loose-плита впереди = КРОМКА в get_edge_distance
Осторожный шаг к проваливающемуся полу проваливал 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>
2026-07-29 16:29:01 +03:00
snark13 5f5eefc9fc PoP roomtest: loose-плита поверх Кида (порт draw_loose + draw_tile_base)
Комната 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>
2026-07-29 16:14:23 +03:00
snark13 517d225091 PoP roomtest: полный порт bumped (seg004) — прижатие Y к полу + hardbump
Комната 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>
2026-07-29 14:23:10 +03:00
snark13 fc69316c2c PoP roomtest: окклюзия падающей плиты соседним полом — ряд из координаты
Комната 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>
2026-07-29 12:28:50 +03:00
snark13 08ebf09d4d PoP roomtest: пометить недостижимые комнаты уровня 1 (13, 18, 24)
Обход графа связей res2001.bin (links @1952) от стартовой комнаты 1: 13, 18
и 24 недостижимы — ссылки наружу у них есть, на них не ссылается никто
(24: L->9, но у 9 R=0).  Несимметричные ссылки ровно у этих трёх, у прочих
21 симметрия полная — признак выкинутых из компоновки комнат.  В таблицу
обхода добавлена пометка, чтобы не гоняться за призраками: рендер кромки
читает колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
считает, так что странный шов там — свойство данных.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:06:47 +03:00
snark13 50aa652dbe sprinter-cc: убрать устаревший комментарий «--w3 несовместим с huge»
Комментарий у блока разбора --w3 утверждал, что режим подразумевает small и
несовместим с huge, хотя код тремя строками ниже huge как раз поддерживает:
резидент делит окно W3 с трамплин-банками (порт 0xE2), crt0_banked запоминает
резидентную страницу и возвращает её дефолтом после загрузки банков, а
трамплин bank.s сохраняет/восстанавливает W3 на каждый __banked вызов.
Следствие, которое теперь тоже записано: резидент -> __banked работает,
обратное невозможно (из банка резидентной страницы в W3 просто нет).

В справке по --w3 дополнено правило «W3-код не переключает страницу W3»:
сюда же относится собственная графическая скобка _bgi_begin/_bgi_end — она
маппит видеобанк ЧЕРЕЗ ТОТ ЖЕ порт 0xE2, и открытая из W3-кода означает
исполнение из видео-ОЗУ (белый экран).  Звать графические примитивы HOME
можно: скобку они открывают и закрывают внутри себя.

Только комментарии, поведение не менялось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:04:18 +03:00
snark13 08c6a8504f PoP roomtest: отладочный обход комнат (+/-) + TODO по редрою пик и idle-skip
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>
2026-07-29 11:58:06 +03:00
snark13 723da3c5c1 PoP roomtest: пики — редрой каждый кадр, полный draw_tile; пламя без heal
Выдвинутая пика живёт ТОЛЬКО в видео-ОЗУ (банк 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>
2026-07-29 11:34:07 +03:00
snark13 e143ee010a PoP roomtest: нажатая кнопка-closer, таблица падающих кусков, фон-покой при запечке
Кнопка-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>
2026-07-28 12:07:29 +03:00
snark13 b106aba59b PoP roomtest: подъём/спуск на ОТКРЫТЫХ воротах (потерянная проверка openness)
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>
2026-07-28 10:59:30 +03:00
snark13 5e09bd9d5a PoP roomtest: непрозрачные нижние грани + edge-триггер бампа + 2 колонки шва
Три фикса, найденные разбором в 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>
2026-07-28 10:47:58 +03:00
snark13 6582154381 PoP roomtest: clip_char (верхняя обрезка спрайта) + подстановка нажатой кнопки
Спуск Кида с кнопки (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>
2026-07-28 09:50:47 +03:00
snark13 c127a4b0d2 PoP: факелы и зелья (chtab_1) + быстрый блит фона gfx_blit_noclip
Графика 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>
2026-07-27 20:53:04 +03:00
snark13 63a3b60524 PoP roomtest: плита-потолок, порядок слоёв по SDLPoP, фиксы окклюзии и Y
Фича — плита-ПОТОЛОК (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>
2026-07-27 17:35:25 +03:00
snark13 86d7615841 PoP: roomtest — объекты/обломки/переходы комнат + арт
Порт 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>
2026-07-23 19:38:23 +03:00
snark13 3d586af031 libc/kbd: raw-клавиатура — вычерпывание FIFO + селективный wipe модификаторов
- kbd_raw_sync: цикл вычерпывания SIO FIFO (не 1 байт/прерывание) —
  фикс залипания клавиш; overrun-wipe сбрасывает только пострадавшие
  клавиши, не модификаторы (typematic их не перечитывает).
- Гайд docs/kbd-games.md; заметка о Rx-overrun в docs/TODO.md; справочник
  скан-кодов в docs/libc-reference.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:12 +03:00
snark13 95c22be9bd libbgi: скролл-примитивы + --w3 + отчёт раскладки памяти
Скролл региона video->video (неактивная страница -> активная, банк 0x50:
копия = скролл + heal цели):
- gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без
  страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256;
- gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход
  без буфера (колонка = accel-burst LD A,A, STOP между read/write делает
  промежуточный OUT Port_Y безопасным), банды по 16 колонок;
- _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t.
Пример examples/scroll.

check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели
(W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank),
не только при --bank.  Док docs/memory-management.md §10.

--w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT,
mkexe -W, tests/w3probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:00 +03:00
snark13 d1bc97c589 roomtest: обломки упавшего loose в комнате снизу (debris)
Loose-пол room1(2,6) при падении даёт кусок, который в оригинале улетает в
комнату снизу (links.down) и приземляется на первый floor, превращая его в
debris (порт move_loose/loose_land seg007).  Для room1(2,6): room2(0,6)=empty
пролёт → room2(1,6)=floor приземление → debris.

Минимальная реализация (без анимации полёта куска, только персистентный
результат — мини-версия P0 из docs/gates_spikes_plan.md):
- pop_map: сигнал pop_loose_fell (tilepos+1 упавшего loose).
- pop_level: pop_room_col_landing(room,col) — ряд первого floor сверху вниз
  (пустые ряды пролетаются), иначе -1.
- roomtest: при pop_loose_fell вычисляет debris-приземление в комнате снизу →
  персистентный override (dbr_room/dbr_pos, живёт между переходами) →
  enter_room применяет поверх загруженных тайлов (floor→debris 0x0E).

Проверено в MAME: провал loose room1(2,6) → debris в room2(1,6), сохраняется
при повторных входах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:26:21 +03:00
snark13 bf7ddfd4da roomtest: L3 переходы вбок (право+лево) + план объектов (кнопки/гейты/пики)
Переходы в соседние комнаты по горизонтали (порт SDLPoP leave_room/
goto_other_room/find_room_of_tile), проверено в MAME (room2↔room3↔room9,
room1↔room5 и т.д.):

- pop_level: pop_room_load теперь извлекает и rightcol (col0 правого соседа),
  как leftcol — для коллизии/рендера правого шва.
- pop_map: pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg) — связи + кромки швов.
  get_tile(col=-1)=lcol (col9 левого), get_tile(col=10)=rcol (col0 правого),
  с гейтом по связи (нет соседа → стена) — порт find_room_of_tile.  check_leave
  (порт leave_room): передний край char_x_left<=54 / char_x_right>=201 (и
  обратные <=57 / >=198) → x∓140, сигнал pop_leave_dir (1=left,2=right).
- roomtest: rcol-массивы; enter_room задаёт edges; обработчик pop_leave_dir
  переключает комнату по pop_room_link.
- Фикс двойного перехода: без коллизии ЛЕВОГО шва Kid, войдя справа в соседа
  (curr_col=-1), стоял «в стене» → in_wall выбрасывал его на второй переход.
  Левый шов (get_tile(-1)=lcol) это чинит.

docs/gates_spikes_plan.md — ПОДРОБНЫЙ самодостаточный план на след. сессию:
интерактивные объекты (RAISE/DROP-кнопки, гейты, пики) + HP/смерть.  Контекст
текущего roomtest, форматы уровня (LINKLOC/LINKMAP @1440/1696), данные room6,
декод связи кнопка(0,2)→гейт room8(0,9), триггер пик (check_spike_below),
точные ссылки SDLPoP, фазы P0(персистентное per-room состояние+trob)/
S(пики+HP/смерть)/B(кнопки+гейты).

NB: L3-вверх (climb-up в комнату сверху) ещё не сделан.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:14:03 +03:00
snark13 7f3e32ddd6 roomtest: L2 — переход в комнату снизу (провал/спуск) + фиксы окклюзии
При пересечении нижней границы комнаты Kid переходит в комнату links.down,
продолжая падать/спускаться (порт SDLPoP goto_other_room/leave_room).

- pop_map check_leave_below (порт leave_room, seg002): триггер по Kid.y>=211
  (не curr_row) — единый для провала И спуска-зацепа/climbdown; репроекция
  координат в кадр комнаты снизу (y-=189, curr_row=y_to_row_mod4) + сигнал
  pop_fell_out.  Убран прежний преждевременный триггер y>=189 из do_fall
  (из-за него climbdown с curr_row=3 ждал y_land[4]=244 → пролёт через комнату).
- roomtest: enter_room(room) — загрузка комнаты в рабочие массивы + отрисовка
  фона в обе страницы; обработчик pop_fell_out переключает комнату по
  pop_room_link(down) или респавнит (нет комнаты снизу).  cur_room-трекинг.
- pop_loose_reset (pop_map) + pop_loose_mob_reset (pop_bg): сброс loose-состояния
  (индексировано позицией тайла) при смене комнаты — иначе течёт в новую.

Фиксы окклюзии при висе/спуске (сверено с SDLPoP):
- other_overlay_tile: пустой тайл НЕ окклюдирует — draw_tile для него рисует
  лишь фоновую сетку (BLUELINE «силуэт кладки»), которая в оригинале уходит в
  y-сортируемый midtable ПОЗАДИ Kid; у нас клалась поверх.  Пропускаем.
- pop_fore_over_kid: порт set_char_collision (seg006:0723) — char_bottom_row =
  y_to_row_mod4(obj_y), обёртка -1 → ряд 3 (Kid НИЖЕ комнаты при спуске), а не
  ряд 0.  Прежний кламп в 0 давал rT>rB → цикл окклюзии не выполнялся, нога
  рисовалась поверх пола.  single-room: ряд 3 клампится в 2 (передняя грань пола).

Проверено в MAME: провал вниз, спуск-зацеп/climbdown с кромки, окклюзия ноги.
Известно (позже): осколки упавшего loose в комнате снизу (перенос mob),
per-room modified-tile tracking (re-entry восстанавливает исходные тайлы), HP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:22:52 +03:00
snark13 21f978d441 roomtest: L1 — данные уровня из сырого res2001.bin вместо хардкода
Отказ от room1_data.h: уровень читается из сырого blueprnt DAT 1.0
(applications/PoP/SDLPoP/data/LEVELS/res2001.bin, формат — Table 6 POP-DAT).

- pop_level.c/.h: уровень грузится ОДИН раз в отдельную EMM-страницу (данные
  с offset 0x100, ISR-стаб в первых байтах — страница безопасна для W0-маппинга,
  как атласы).  pop_room_load(room) извлекает комнату в массивы приложения (W2):
  fg[30] (тайл-код = байт & 0x1F, верхние биты-модификаторы пока отброшены,
  как делал render_room.py), bg[30] (raw) + срезы соседей для кромок — правый
  столбец left-комнаты (leftcol), верхний ряд down-комнаты (belowrow).
  pop_room_link() — связи для будущих переходов (L2/L3).
- Рабочая копия ТЕКУЩЕЙ комнаты — в обычной памяти (W2, мутабельная: loose→
  empty); страница уровня маппится в W0 только на время извлечения.
- Проверено в MAME: комната 1 рисуется идентично.  Данные сверены байт-в-байт
  (fg&0x1F, bg raw, leftcol из room5 совпали); belowrow теперь из реального
  room2 (links.down=2), а не из прежнего частично-выдуманного хардкода.
- Память (small): DATA до 0xA260, стек от 0xBFFF — запас ~7.5КБ; EMM-страница
  уровня вне W1/W2.

NB: res2001.bin берётся из клона SDLPoP (data/ в .gitignore) — как и генерация
атласов через pop_pack_bg.py; в наш репозиторий сырой уровень не тащим.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:52:31 +03:00
snark13 8b30dc20c8 roomtest: loose-полы — тряска (knock), падение с окклюзией, фикс дабл-буфера
Проваливающиеся полы в 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>
2026-07-20 15:27:47 +03:00
302 changed files with 47603 additions and 1337 deletions
+7
View File
@@ -113,3 +113,10 @@ mame/
# Claude Code local settings (per-machine, not for the repo)
.claude/
# Тяжёлые справочные материалы, НЕ версионируются: docs/extra — архивы
# исходников (~570 МБ, четыре почти одинаковых bad_apple), docs/sources —
# клоны чужих репозиториев (Sprinter-BIOS, Estex-DSS, SaymanNsk) со своими
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
docs/extra/
docs/sources/
+16 -4
View File
@@ -1,10 +1,14 @@
# Sprinter C Compiler — top-level Makefile
#
# make build host tools, libc archive, all tests, all apps
# make build host tools, libc archive, all tests
# make tools build only host tools (mkexe)
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
# make tests build all libc feature tests under tests/
# make examples build all real applications under examples/
# make examples build all real applications under examples/ (НЕ входит
# в `make all`: это регрессная сборка, а examples/ —
# крупные приложения, которые её только замедляют
# (mdview компилируется минутами) и ничего нового про
# libc не показывают. Собирать явно перед `make floppy`.)
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
# make check run mkexe unit tests
# make clean remove all build artefacts
@@ -39,9 +43,9 @@ DATA_FILES := \
examples/mdview/SAMPLE.MD
.PHONY: all tools lib tests examples check clean sdcc floppy \
size-check size-baseline $(TESTS) $(APPS)
size-check size-baseline host-tests $(TESTS) $(APPS)
all: tools lib tests examples
all: tools lib tests
tools:
$(MAKE) -C toolchain/mkexe
@@ -73,6 +77,14 @@ floppy: tests examples tests/seek/big.txt
@echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
# Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат
# рядом с кодом, который проверяют. MAME не нужна, идут за секунды;
# ucsim идёт в комплекте нашего SDCC.
HOST_TEST_DIRS := testkit applications/PoP/roomtest/tests-host
host-tests:
@for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
size-check:
python3 toolchain/size_check.py
+13 -1
View File
@@ -36,7 +36,9 @@ LIB := $(PROJ_ROOT)/lib/sprinter.lib
MAME_DIR := $(PROJ_ROOT)/mame/v306
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
MAKE_DISK := $(MAME_DIR)/make_disk.py
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
RUN_MAME := $(MAME_DIR)/run_mame.sh
# Optional knobs — see top of file.
@@ -85,4 +87,14 @@ floppy: $(EXAMPLE).exe
run: floppy
cd $(MAME_DIR) && ./run_mame.sh
.PHONY: all clean floppy run
# `make hdd` packs this program (+ optional EXTRA_DATA files) into the MAME
# HDD image mounted as disk D: (-hard2 test_hdd.chd). Гораздо быстрее FDD —
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
hdd: $(EXAMPLE).exe
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
@echo
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
.PHONY: all clean floppy run hdd
+88
View File
@@ -0,0 +1,88 @@
# Prince of Persia → ZX Sprinter — правила подпроекта
Порт Prince of Persia (DOS/Apple II) на Sprinter Sp2000 поверх нашего
sprinter-cc / libc / libbgi. Действуют правила корневого
`CLAUDE.md` (сборка, libc, ABI, MAME-автотест); ниже — только специфика PoP.
Общение и комментарии — на русском.
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
**`SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ авторитетный
источник того, как оригинальный движок это делает.** Правило без исключений:
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
константы) — СНАЧАЛА найди и прочитай соответствующий код в `SDLPoP/src/`,
и портируй по нему. Не пиши по памяти, не выводи логику «из общих
соображений», не угадывай значения — это источник багов, которые потом
ловятся в MAME часами.
2. **По любому вопросу «как в оригинале должно быть»** (что окклюдит что,
в каком порядке слои, когда меняется тайл, какая скорость/задержка,
что делает такой-то кадр анимации) — ответ ищи в `SDLPoP/src/`, а не
строй гипотезу. Если в SDLPoP не нашёл — это повод копать дальше в
исходнике, а не додумывать.
3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока
не доказано обратное (наша платформа/ABI требует отличия — тогда явно
зафиксировать почему в комментарии).
Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/
seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/
слои/окклюзия, `seg009` чтение ресурсов. Слои окклюзии у нас = слои SDLPoP.
См. memory `pop_check_sdlpop_first`.
Вторичные референсы (когда в SDLPoP непонятно/нужен другой ракурс):
- `Prince-of-Persia-Apple-II/` — оригинальный 6502-исходник 1989 (Мехнер).
- `PR/` (github.com/NagyD/PR, GPLv2) — Princed Resources.
- `mininim/` — независимая реализация.
Все эти папки — **справочник логики/структур/констант и источник ассетов**,
но НЕ код для копирования (лицензии несовместимы, наш ABI другой): читаем
и переписываем под наш движок, а не вставляем куски.
## Ассеты
Готовые распакованные VGA-256 ассеты (то, что нужно под 320×256×256) —
`SDLPoP/data/` (`res<id>.png`/`.pal`/`.bin`). Брать оттуда, а НЕ писать свой
декодер DOS `.DAT`. Локальные `.DAT` — в `MSDOS/`.
**Каноническая спецификация форматов `.DAT` — `docs/POP-DAT-FormatSpecifications.pdf`**
(грепаемая копия — `docs/POP-DAT-FormatSpecifications.txt`): первоисточник
Princed для DAT v1.0 (контейнер/индекс/чек-сумма, кодеки RLE/LZG, палитры,
формат уровней, звук), на нём построены и SDLPoP, и Princed Resources. Наши
разборы (`docs/MSDOS_RESOURCE_FORMAT.md` / `docs/APPLEII_RESOURCE_FORMAT.md` /
`docs/README.md`) — практические заметки/сверки; при расхождении источник
истины — спецификация. Формат уровня почти идентичен в Apple II и DOS.
Упаковка ассетов под Sprinter (атласы `.atl`, палитра) — python-скрипты в
`toolchain/` (`render_room.py`, `pop_pack_bg.py`, `pop_pack_kid.py`,
`pop_extract_kid_data.py`). Их дёргают Makefile'ы тестов.
## Структура папки
- `docs/` — планы и форматы; **индекс с отметками актуальности —
`docs/README.md`**, начинать чтение оттуда. Ключевое:
`levels_plan.md` (следующий этап), `layout_plan_v2.md` (раскладка кода по
окнам/банкам + скорость отрисовки), `PORT_PLAN.md` (карта фаз со
статусами).
- `roomtest/`**активная разработка**: уровень 1 целиком (Kid, стражи,
ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи —
`roomtest/TASKS.md`, открытые баги — `roomtest/bug_list.md`, закрытые с
разбором корней — `roomtest/bug_closed.md`.
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
- `SDLPoP/`, `PR/`, `Prince-of-Persia-Apple-II/`, `mininim/`, `MSDOS/`
референсы/оригинальные данные (см. выше).
## Ключевые архитектурные решения (memory/)
- `pop_port_project` — общий статус порта.
- `pop_banking_architecture` — будущее: big+BANK_W1, графику нельзя в W3,
один файл = один банк = прямые вызовы, main резидентен.
- `pop_background_strategy` — фон = композиция тайлов в рантайме (вариант 3).
- `pop_kid_plan` / `pop_hang_state` / `pop_fore_layer` /
`pop_fall_debug_baseline` — этапы Kid.
- `kbd_raw_fifo_drain` — held-state клавиатуры (единственный принципиальный
пробел движка, закрыт `<kbd_raw.h>`): вычерпывать FIFO SIO циклом.
- `png_strip_padding_tradeoff`, `pop_tile_atlas_palette_merge` — квирки
упаковки ассетов.
+42
View File
@@ -0,0 +1,42 @@
# Prince of Persia на ZX Sprinter
Порт Prince of Persia на компьютер Sprinter Sp2000 поверх нашего
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
**Состояние (2026-08-01): играется весь уровень 1** — комнаты и переходы,
Kid со всем набором действий, ловушки, ворота, дверь уровня, меч и бой,
стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука,
таймера/HUD, сохранений.
- Что в работе прямо сейчас — [`roomtest/TASKS.md`](roomtest/TASKS.md).
- Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md).
- Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
- Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
## Что где
| Папка | Назначение |
|-------|-----------|
| `roomtest/` | **Активная разработка.** Уровень 1 целиком: фон композицией тайлов, Kid (seqtbl-анимация, ввод, коллизия, падение, зацеп, окклюзия), ловушки, ворота, стражи, бой. Свой README/CLAUDE/TASKS. |
| `docs/` | Планы и разбор форматов ресурсов Apple II / DOS — см. индекс в [`docs/README.md`](docs/README.md). |
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
| `SDLPoP/`, `PR/`, `mininim/` | Референсные реализации движка (GPL) — читаем логику/константы, НЕ копируем код. `SDLPoP/data/` — источник распакованных VGA-ассетов. |
| `Prince-of-Persia-Apple-II/` | Оригинальный 6502-исходник 1989 г. |
| `MSDOS/` | Локальные `.DAT`-ресурсы DOS-версии. |
## Референсы = только справочник
`SDLPoP/`, `PR/`, `mininim/`, `Prince-of-Persia-Apple-II/` используются как
справочник структур/логики и как источник готовых ассетов — их код НЕ
копируется в наш порт (лицензии несовместимы, ABI другой). Любая механика
сверяется с `SDLPoP/src/` **до** реализации.
## Форматы ресурсов
Формат уровня почти идентичен в Apple II и DOS (2304 / 2305 байт,
`blueprnt`). Графика различается принципиально, но брать распакованные PNG
из `SDLPoP/data/` практичнее, чем декодировать сырой `.DAT`. Подробности —
[`docs/README.md`](docs/README.md).
+16 -1
View File
@@ -1,6 +1,21 @@
# Prince of Persia — Kid (персонаж): анализ и план
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Kid играется целиком: интерпретатор
> `seqtbl` + `frame_table` (`roomtest/pop_kid.c`), диспетчер `control()`
> (`pop_ctrl.c`), коллизия/физика/зацеп (`pop_map.c`), бой и HP. Модель
> персонажа стала общей: `Char`-окно (`pop_state.c`) обслуживает и Кида, и
> стражей. Таблицы кадров и `seqtbl` уехали из `_CODE` в EMM-страницу
> (`kid_data.bin`, см. `layout_plan_v2.md` шаг 1).
>
> Отступление от §2.1 плана: выбран ПАДДИНГ кадров (общий канвас), а не
> per-frame offset — компромисс зафиксирован в `PORT_PLAN.md §6.1`.
>
> **Документ оставлен как СПРАВОЧНИК по модели персонажа** (`char_type`,
> категории `actions_*`, устройство `play_seq`, объём спрайтов) — он нужен
> при портировании остальных акторов (скелет, тень, визирь). Текущие
> задачи — `../roomtest/TASKS.md`.
Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c`
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
+12 -2
View File
@@ -8,8 +8,18 @@
(например: смещение+размер последней записи таблицы точно совпадает с
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
Для справки при последующей реализации (порт на ZX Sprinter) стоит держать в
уме два внешних проекта:
**Основной источник спецификации формата — `POP-DAT-FormatSpecifications.pdf`**
(и его текстовая конверсия `POP-DAT-FormatSpecifications.txt` в этой же папке,
для grep/цитирования): *«Prince of Persia — Specifications of File Formats»*,
Princed Development Team, 2008 — каноническая спецификация формата `DAT v1.0`,
на которой построен и SDLPoP, и Princed Resources. Разбирает контейнер, индекс,
чек-сумму, кодеки изображений (RLE / LZG), палитры, формат уровней (room
mapping, wall-drawing, room-linking, guards, start position, door events),
звук (digital waves / MIDI / PC speaker), бинарные файлы и Mac-варианты. При
любом расхождении между эмпирическими находками ниже и этим документом —
источником истины считать спецификацию (сверять §-номера: её §3.x).
Дополнительно как справка при реализации (порт на ZX Sprinter):
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
File diff suppressed because it is too large Load Diff
+98 -52
View File
@@ -1,8 +1,29 @@
# Prince of Persia на ZX Sprinter — план порта
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
## СТАТУС (обновлено 2026-08-01)
Документ составлен 2026-07-15 как план «с нуля» и с тех пор во многом
исполнен. Читать его надо так:
| Раздел | Что с ним сейчас |
|--------|------------------|
| §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) |
| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS.md` (KBD-1) |
| §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) |
| §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) |
| §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой |
| §6 модель движения | **сделано**: `play_seq` + `frame_table` оригинала, не физика с нуля |
| §7 фазы | см. отметки статуса прямо в разделе |
| §8 риски | п.1 закрыт, п.3 закрыт (28 страниц-атласов Кида), п.2/п.4 — см. отметки в разделе |
| §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` |
**Где смотреть текущее состояние, а не план:** `../roomtest/README.md`
(что играется), `../roomtest/TASKS.md` (что в работе), `levels_plan.md`
(следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам).
---
Опирается на
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
@@ -227,6 +248,13 @@ dirty-биты, heal против фона через ОЗУ-копию); `room_
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
> **Закрыт (исторический раздел).** PoC в `poc/` свою задачу выполнил и
> дальше не развивается: управление ощущается как PoP, held-state работает.
> Всё, что ниже про плейсхолдер-персонажа и приблизительную дугу прыжка,
> — уже неправда для активной ветки: в `roomtest/` стоит настоящая графика
> Кида и авторские таблицы кадров (§6). Раздел оставлен ради истории
> решений (в частности §5.1 — почему сначала был плейсхолдер).
**Объём**: одна комната (например Level 1, экран старта Кида), без
переходов между экранами, без стражников (стретч-цель, не обязательна).
@@ -364,45 +392,53 @@ memory/png_strip_padding_tradeoff.
## 7. Полноценное приложение — фазы (после PoC)
Порядок — по риску и зависимостям, не по геймплейной важности.
**Отметки статуса — на 2026-08-01.**
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
подтверждения пользователем).
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
не обязательно разворачивать в C-struct с указателями).
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
хватит текущего лимита — см. риск в §8).
**Фаза 0 — инфраструктура порта** — **СДЕЛАНА**, но иначе, чем задумано:
- Хелд-стейт клавиатуры по §2 — сделан.
- Конвертер уровней не понадобился: `res200N.bin` из `SDLPoP/data/LEVELS`
кладётся на образ как есть и читается по офсетам в рантайме
(`roomtest/pop_level.c`), уровень живёт в EMM-странице.
- Конвертер фона в растры **отменён осознанно** (§4): фон собирается
тайлами в рантайме. Спрайты — `toolchain/pop_pack_bg.py` /
`pop_pack_kid.py` / `pop_pack_guard.py` → атласы `.atl` (Kid — 28
страниц, риск §8 п.3 закрыт).
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
**Фаза 1 — Кид, полный набор действий** — **СДЕЛАНА**: стоять/идти/бежать/
тормозить/разворот/прыжки/повисание/подтягивание/спуск/приседание/
осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
комнатами во все четыре стороны. Осталось: **старт по данным уровня**
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
`../roomtest/TASKS.md`.
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
**Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
`LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
потолка), зелья, дверь уровня (открывается), факелы. Подробности и
справочник — `gates_spikes_plan.md`.
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
спереди/сзади».
**Фаза 3 — бой** — **СДЕЛАНА в объёме обычного стражника**: подбор и
выхватывание меча, стойка, удар/парирование, коллизия клинков, HP обеих
сторон, смерть; ИИ стража (замечает Кида, подходит, боевые ветки),
персистентность трупа между комнатами.
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
§3), толстый стражник/визирь (общая база анимации с визирем).
**Фаза 4 — разнообразие противников** — **НЕ НАЧАТА**. Скелет нужен на
уровне 3, толстый — на 6, тень — на 12, визирь — на 13; привязка
«уровень → тип стража» (`tbl_guard_type`) описана в `levels_plan.md` §1.
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
**Фаза 5 — звук** — **НЕ НАЧАТА**: CBL-эффекты (шаги, удары, двери,
падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как
опциональный дешёвый бипер без CBL, если формат подтвердится простым
парсингом. Опкод `SOUND` в `play_seq` пока просто съедает свой аргумент —
точки вызова уже на месте.
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
геймплейно не критично.
**Фаза 6 — оболочка** — **НЕ НАЧАТА**: титры, меню/выбор уровня, HUD
(таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по
минимуму, геймплейно не критично. Полоса HP — единственное, что уже есть.
**Между Фазами 4 и 5 вклинивается то, чего в этом плане не было:
переход между УРОВНЯМИ** (загрузка следующего уровня, второй тайлсет
palace, потабличные различия уровней). Отдельный документ —
`levels_plan.md`.
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
@@ -418,23 +454,33 @@ tiny/small.
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
несколько анимированных ловушек может приблизиться к лимиту
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
уровням (сколько объектов одновременно активно в худшем экране).
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
атласов на актора с переключением по фазе действия (стоять/идти отдельно
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
атласы — оценить фактический байтовый вес конвертированных кадров Кида
прежде чем проектировать.
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
Sprinter; если оригинал считался на другой частоте — потребуется
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
визуально быстрее/медленнее эталона.
1. ~~**Held-state клавиатуры** (§2)~~**закрыт** (`<kbd_raw.h>`). Открытый
остаток — не «есть ли held-state», а потеря байт при аккордах
Shift+стрелка: `../roomtest/TASKS.md`, KBD-1.
2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
спрайтовый движок для персонажей не используется, поэтому лимит
«~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
(~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()`
в `roomtest.c`. План выжимания — `../roomtest/TASKS.md` (CLIP-1) и
`../roomtest/bug_list.md` (T-1/T-2).
3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
мульти-страничного формата `.atl` не потребовалось. Побочно
подтвердился компромисс паддинга (§6.1).
4. **Тайминг оригинала** — **ОТКРЫТ, и сверка 2026-08-01 показывает
расхождение.** Цифры оригинала (SDLPoP): базовый таймер `BASE_FPS = 60`
(`types.h:1373`), логический кадр игры — `base_speed = 5` тиков
(`data.h:869`), то есть **83.3 мс (12 лог. кадров/с)**; в бою
`fight_speed = 6` → **100 мс (10/с)**. У нас (`roomtest.c`) — три
ожидания `gfx_wait_vsync()` на итерацию, то есть **60 мс (16.7/с)** и
без отдельной скорости боя. Значит **игра идёт примерно на 39 %
быстрее эталона**. Точное соответствие даёт 4 ожидания vsync (80 мс
против 83.3) и 5 в бою (100 мс — совпадает точно).
Проверять не «на глаз», а секундомером по одинаковому отрезку
(SDLPoP рядом на том же экране), и только после того, как кадр
перестанет иногда вылезать за период (см. п.2) — иначе замедление
спрячет проблему бюджета вместо того, чтобы её показать.
---
+50 -2
View File
@@ -1,4 +1,48 @@
# Форматы ресурсов Prince of Persia — сводка
# `applications/PoP/docs` — индекс + сводка по форматам ресурсов
## Индекс документов (актуальность на 2026-08-01)
**Живые планы — читать перед работой:**
| Документ | О чём |
|----------|-------|
| [`../roomtest/TASKS.md`](../roomtest/TASKS.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) |
| [`../roomtest/bug_list.md`](../roomtest/bug_list.md) | Открытые баги roomtest (закрытые — в `bug_closed.md` рядом) |
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди |
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии из `bug_closed.md`, дифф против SDLPoP |
| [`ideas_backlog.md`](ideas_backlog.md) | Осознанно отложенные гипотезы (мышь, PRNG) |
| [`prng_alternatives.md`](prng_alternatives.md) | Запасные генераторы, если упрёмся в бюджет кадра |
**Исполненные планы, оставленные как справочники:**
| Документ | Чем ещё полезен |
|----------|-----------------|
| [`PORT_PLAN.md`](PORT_PLAN.md) | Общая карта фаз со статусами; §6 (модель движения), §10 (режим памяти) |
| [`KID_PLAN.md`](KID_PLAN.md) | Модель персонажа: `char_type`, `actions_*`, устройство `play_seq` — нужна для скелета/тени/визиря |
| [`gates_spikes_plan.md`](gates_spikes_plan.md) | Раскладка объектов уровня 1 по комнатам, декод `LINKLOC`/`LINKMAP`, точные ссылки на seg-код |
**Форматы ресурсов** (ниже по этому файлу): `POP-DAT-FormatSpecifications.pdf`
/ `.txt` (первоисточник), `APPLEII_RESOURCE_FORMAT.md`,
`MSDOS_RESOURCE_FORMAT.md`.
Удалены 2026-08-01 как полностью исполненные и перекрытые кодом:
`clip_char_plan.md`, `double_buffer_plan.md`, `loose_floors_plan.md`,
`size_optimization_plan.md` (его §8 про скорость отрисовки перенесён в
`layout_plan_v2.md` §9). Ищутся в истории git, если понадобятся.
---
## Форматы ресурсов — сводка
**Каноническая спецификация форматов**`POP-DAT-FormatSpecifications.pdf`
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
*«Prince of Persia — Specifications of File Formats»*, Princed Development Team,
2008. Это первоисточник формата `DAT v1.0` (контейнер, индекс, чек-сумма,
кодеки RLE/LZG, палитры, уровни, звук), на котором построены и SDLPoP, и
Princed Resources. Документы ниже — наши практические заметки/сверки; при
расхождении источником истины считать спецификацию.
Цель этих документов — подготовить почву для будущего порта Prince of Persia
на ZX Sprinter, разобрав, как устроены ресурсы игры в двух доступных нам
@@ -62,7 +106,11 @@ DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`B
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
декодер сжатия пикселей DOS `.DAT`.
## Что дальше (не сделано в этом заходе)
## Что дальше по форматам (не сделано и пока не нужно)
Порт читает уровень напрямую из `res200N.bin` (`roomtest/pop_level.c`), а
графику берёт из распакованных PNG `SDLPoP/data/` — поэтому ни один пункт
ниже сейчас не блокирует работу.
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
@@ -1,69 +0,0 @@
# Double buffer (два экрана + флип) — план
Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей)
и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком
слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не
промежуточные состояния heal-рендера). **Тумблер обязателен**
однобуферный режим удобнее для отладки багов рисования.
## Что уже готово (libbgi — трогать НЕ нужно)
- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает
`_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы),
`gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS).
- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов.
- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync();
set_visible_page(hidden);`
- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры
(page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ.
## Текущая модель рендера roomtest (однобуфер)
`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты
рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT).
Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()`
(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick`
→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее
бланка, луч ловит промежуток.
## Работа на стороне PoP
1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1
(теневая копия одна — общая, из неё heal'ит любая страница).
2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync.
Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync —
plane-параметр).
3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в
массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)`
восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем
через страницу). То же для fore/пол-оверлея, если они рисуют вне
футпринта Кида.
4. **Ping-pong в цикле**:
```
uint8_t back = dbuf ? (front ^ 1) : 0;
gfx_set_draw_page(back);
kid_heal(back); kid_tick; phys; kid_draw; fore;
gfx_wait_vsync();
if (dbuf) { gfx_set_visible_page(back); front = back; }
```
5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] —
бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или
compile-флагом.
6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед
свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным
счётом кадров, чтобы скорость игры не изменилась.
## Порядок
- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page
heal (проверить, что флип работает, фон корректен на обеих).
- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида.
- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет).
- D4: пейсинг (fps_div) — вернуть исходную скорость.
## Связанные
Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]];
heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors
[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в
обе страницы по той же дисциплине.
+279
View File
@@ -0,0 +1,279 @@
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Все фазы плана (P0 персистентный
> per-room `room_modif`, S пики, B кнопки+ворота) сделаны и играются:
> `roomtest/pop_trob.c` (trob-диспетчер, `LINKLOC`/`LINKMAP`, ворота, дверь
> уровня, факелы, зелья), `pop_map.c` (HP, смерть на пиках, урон падения),
> `pop_redraw.c` (пометки перерисовки вместо прямых блитов). Ограничения
> из §0 закрыты: тайлы персистентны, HP/смерть есть, loose обобщён в trob;
> L3-вверх (climb-up в комнату сверху) тоже сделан (`pop_leave_dir = 3`).
> Из §5 остаётся открытым только **переход на следующий уровень через дверь
> уровня** — он вынесен в `levels_plan.md`.
>
> **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка
> объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные
> ссылки на механику SDLPoP) продолжают экономить время при отладке.
> Текущие задачи — `../roomtest/TASKS.md`.
Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт
«с чистого листа» (пустой контекст). Всё сверено с
`applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
перед кодингом читать соответствующий код seg*.c, не гадать.
---
## 0. КОНТЕКСТ: текущее состояние `applications/PoP/roomtest` (что уже готово)
roomtest — живой прототип порта PoP: комната 1 уровня 1 живой композицией
тайлов + Kid (анимация/управление/коллизия/падение/зацеп/переходы). Собрать:
`cd applications/PoP/roomtest && make`. Тест в MAME: см. memory
`mame_mcp_bridge`/`mame_hdd_test_disk` (канонический цикл: `make` → пересобрать
`mame/v306/IMG/test_hdd.chd` через `toolchain/make_hdd.sh` со всеми ассетами →
рестарт `run_bridge.sh``resume` → ~13с бут → `type_string("d:{ENTER}roomtest.exe{ENTER}")`).
Отладка: клавиши `1`=freeze / `2`=resume в roomtest; MCP-мост `mame-z80`
(read_logical_memory, set_breakpoint, disassemble); адреса символов —
`.sprinter-cc-roomtest/roomtest.map` (сдвигаются при пересборке!).
### Модули (все в `applications/PoP/roomtest/`)
- `roomtest.c` — главный цикл (дабл-буфер 2 стр.), `enter_room(room)`,
обработчики переходов. File-static рабочие массивы (W2):
`room_fg[30]`, `room_bg[30]`, `lcol_fg/lcol_bg[3]`, `rcol_fg/rcol_bg[3]`,
`below_fg[10]`, `cur_room`.
- `pop_level.c/.h`**уровень из файла** (Фаза L1):
- `pop_level_load("res2001.bin")` — читает сырой blueprnt в EMM-страницу
(данные с offset `0x100`, ISR-стаб как атлас).
- `pop_room_load(room, fg,bg, lcol_fg,lcol_bg, rcol_fg,rcol_bg, below_fg)`
извлекает комнату (fg маскирован `&0x1F`, bg raw) + срезы соседей:
leftcol=col9 левого соседа, rightcol=col0 правого, belowrow=row0 нижнего.
- `pop_room_link(room, side)` — связь (side 0=L,1=R,2=U,3=D; 0=нет).
- `pop_level_start_room/pos/dir()`.
- `pop_bg.c/.h` — отрисовка тайлов (порт seg008 draw_tile), fore-окклюзия над
Kid (`pop_fore_over_kid`, порт set_char_collision+redraw_at_char/char2),
**loose-полы** (shake/bake/mob). `draw_tile` — статическая, знает
`draw_gate_back` (грань гейта из левой комнаты).
- `pop_kid.c/.h` — анимация Kid (интерпретатор seqtbl `play_seq`, порт seg006),
`Kid` struct (frame,x,y,dir,curr_col,curr_row,action,fall_x,fall_y,repeat,
curr_seq); `knock`-флаг; `kid_cur_dx/flags`, `kid_fp_*` (футпринт).
- `pop_ctrl.c/.h` — ввод (порт seg005 control) через `<kbd_raw.h>`.
- `pop_map.c/.h` — коллизия/физика (порт seg005/006). Ключевое:
- `pop_map_set(fg)` — карта текущей комнаты.
- `pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg)` — связи + кромки швов для
коллизии (`get_tile(col=-1)`=lcol, `get_tile(col=10)`=rcol; порт
find_room_of_tile).
- `pop_phys_tick()` — кадр физики (fall/land/wall/knock/leave).
- Переходы: `pop_fell_out` (вниз, y>=211), `pop_leave_dir` (1=left,2=right,
x∓140).
- **loose-состояние**: `pop_loose_modif[30]` (публично, читает pop_bg),
`loose_bake[30]`, `loose_rest[30]` (static); `pop_loose_tick()`,
`pop_loose_reset()` (сброс при смене комнаты).
### Что сделано по фазам
- **L1** — данные уровня из файла (room1_data.h удалён). Коммит `21f978d`.
- **L2** — переход в комнату снизу (провал/спуск), фиксы окклюзии. Коммит `7f3e32d`.
- **L3** — переходы вбок (право+лево) через швы. **Не закоммичено** на момент
написания (вместе с этим планом). **L3-вверх (climb-up в комнату сверху) —
НЕ сделано.**
- **Loose-полы** (тряска knock / падение mob+окклюзия) — коммит `8b30dc2`.
### Известные ОГРАНИЧЕНИЯ (важно для этого плана)
1. **Нет персистентности тайлов**: `enter_room``pop_room_load` каждый раз
перезагружает ИСХОДНЫЕ тайлы из level-страницы. Изменения (упавший loose,
открытый гейт) при повторном входе ТЕРЯЮТСЯ. Для кнопок/гейтов это
блокер (см. P0).
2. **Нет HP/смерти** Кида (нужно для пик).
3. Loose-механика — частный случай trob (нужно обобщить).
---
## 1. ДАННЫЕ УРОВНЯ (формат, offsets, объекты)
Сырой `res2001.bin` (2305 Б) = blueprnt DAT 1.0 (Table 6 в
`POP-DAT-FormatSpecifications.txt`). Читается в EMM-страницу с offset `0x100`.
Тайл-код = байт `& 0x1F`; верхние биты (модификатор BLUETYPE) сейчас отброшены.
| Блок | Offset | Размер |
|------|--------|--------|
| foretable (fg) | 0 | 720 (24 комн × 30) |
| backtable (bg=modifier) | 720 | 720 |
| **LINKLOC** (doorlink1) | **1440** | 256 |
| **LINKMAP** (doorlink2) | **1696** | 256 |
| links (roomlinks) | 1952 | 96 (24×{L,R,U,D}) |
| start_position | 2112 | 3 (room,pos,dir) |
Тайл-коды: `0x00`empty `0x01`floor `0x02`**SPIKE** `0x03`pillar `0x04`**GATE**
`0x06`**DROP-кнопка(closer)** `0x0B`loose `0x0F`**RAISE-кнопка(opener)**
`0x10`lvldoor-L `0x11`lvldoor-R `0x13`torch `0x14`wall.
### Объекты уровня 1 (по комнатам)
```
room 5: DROP(0,2)m11 RAISE(0,4)m9 GATE(0,5)m2 RAISE(0,6)m8 GATE(0,9)m1
room 6: RAISE(0,2)m7 SPIKE(2,3) SPIKE(2,4) <-- тестовая
room 7: RAISE(0,2)m6 GATE(0,9)m2
room 8: RAISE(0,6)m5 GATE(0,9)m2 RAISE(1,7)m4
room 9: RAISE(0,0)m3 (+ lvldoor(1,3)/(1,4) — выход на level2, отложено)
room12: RAISE(0,3)m2 GATE(0,9)m2 SPIKE(2,4)
room10/13/14/16/19/24: только SPIKE
room20: DROP(1,4)m1 RAISE(1,7)m0
```
(m = modifier тайла = ИНДЕКС в LINKLOC/LINKMAP.)
### Room6 (тестовая) — раскладка
```
fg row0: 13 01 0F 00 03 01 01 13 01 03 (0,2)=RAISE-кнопка, (0,0)/(0,7)=torch
fg row1: 14 14 14 00 14 14 14 14 14 14 (1,3)=empty
fg row2: 14 14 14 02 02 14 14 14 14 14 (2,3)(2,4)=SPIKE
links: L=8 R=2 U=5 D=0
```
- **Шахта пик**: col3 (row0=empty, row1=empty, row2=spike) + col4 (spike).
- **Гейт, видимый у ЛЕВОЙ кромки room6, — это гейт room8 (0,9)**, отрисованный
в col0 room6 через левый шов (L=8, leftcol=col9 room8). В room6 гейта НЕТ.
### Связь кнопка→цель (декод LINKLOC/LINKMAP), проверено:
- `get_doorlink_tile(i) = d1[i] & 0x1F`
- `get_doorlink_next(i) = !(d1[i] & 0x80)` (0 = конец цепочки)
- `get_doorlink_room(i) = ((d1[i]&0x60)>>5) + ((d2[i]&0xE0)>>3)`
- `get_doorlink_timer(i) = d2[i] & 0x1F`
- где `d1`=LINKLOC@1440, `d2`=LINKMAP@1696. Цепочка: idx++ пока next.
**Кнопка room6 (0,2) mod=7 → цель: room8 tile(0,9) = ГЕЙТ.** Подтверждено
в SDLPoP-скринах: нажатие кнопки поднимает решётку у левой кромки room6.
Связь КРОСС-КОМНАТНАЯ (кнопка в room6, гейт в room8) и задаётся таблицей,
а НЕ позицией. Кнопка может открывать НЕСКОЛЬКО гейтов в разных комнатах.
---
## 2. МЕХАНИКА SDLPoP (точные ссылки)
### 2.1 Trob-система (анимируемые тайлы)
- `add_trob(room,tilepos,type)` seg007:0A5A — в список анимируемых.
- Каждый кадр `redraw_needed_tiles`/`process_trobs` продвигает; диспетч по
типу тайла → `animate_button/animate_door/animate_spike/animate_loose`
(seg007:0033+ таблица `animate_*`).
- Состояние тайла хранится в `curr_room_modif[tilepos]` (per-room modifier).
- У нас есть частный случай для loose (`pop_loose_tick`+`pop_loose_modif[30]`).
### 2.2 Пики (spike)
- **Триггер выдвижения** — `check_spike_below()` seg006:1199 (зовётся в
физике Кида каждый кадр): для каждой колонки футпринта Кида
(`get_tile_div_mod_m7(char_x_left)`..`char_x_right`) идёт ВНИЗ от
`Char.curr_row` через НЕ-floor тайлы; если встретил `tiles_2_spike`
`start_anim_spike(room,tilepos)`. → Кид у края (0,2) правым краём задевает
col3 → скан вниз col3 (empty/empty/spike) → пики вылезают.
- `start_anim_spike` seg007:596: если `modif<=0`: `modif==0` → add_trob(type1)
+ звук; `modif<0` (кроме 0xFF disabled) → `modif=0x8F`.
- `animate_spike` seg007:317: автомат по modif — выдвиг `++modif` (1..4; на 5 →
`0x8F`; на 9 → 0, trob кончился); убирание `& 0x80``--modif` (на 0 → `=6`).
`0xFF` = disabled (не двигать).
- `is_spike_harmful` seg007:1178: modif `0/-1`→0 (безопасно); `<0`→1;
`1..4`→2; `>=5`→0.
- **Смерть**: `check_spiked` seg006:0968 — если тайл под Кидом = spike И harmful
И кадр бега (7..14) / старта прыжка (34..39) с harmful>=2, ИЛИ кадр приземления
(43/26) с harmful!=0 → `spiked()`. Падение на пики — отдельный путь (см.
`is_dead` seg006:1907, frame_177_spiked). Осторожный ШАГ по невыдвинутым — ок.
### 2.3 Кнопки
- `trigger_button(playsound, button_type, modifier)` seg007:0C53: modifier =
индекс LINKLOC. `link_timer = get_doorlink_timer(mod)`; если `!=0x1F`
(не заклинено): `set_doorlink_timer(mod,5)`; если был `<2``add_trob`
(кнопка нажимается) + звук; затем `do_trigger_list(mod, button_type)`.
- `do_trigger_list` seg007:09E5: идёт по цепочке LINKLOC от idx, для каждой
цели `trigger_1(target_type,room,tilepos,button_type)` → если >=0
`add_trob(room,tilepos,result)`.
- `animate_button` seg007:0D3A: `timer=get_doorlink_timer(mod)-1`;
`set_doorlink_timer(mod,timer)`; `timer<2` → кнопка отжимается.
- Когда Кид ВСТАЁТ на кнопку: `check_press`-путь seg006 (opener → trigger_button,
closer → тоже; если Кид мёртв — `died_on_button`). RAISE=`tiles_15_opener`
(0x0F), DROP=`tiles_6_closer` (0x06).
### 2.4 Гейты
- `trigger_1` seg007:0999 → для `tiles_4_gate``trigger_gate`.
- `trigger_gate(room,tilepos,button_type)` seg007:092C: modif = высота открытия.
opener: `0xFF`→игнор; `>=188`(открыт)→держать `238`; иначе `modif=(modif+3)&0xFC`,
return 1 (открывать). closer/иначе: `modif!=0` → return 3 (закрыть быстро).
- `animate_door` seg007:0522: анимация открытия/закрытия; `door_delta[]={-1,4,4}`,
`gate_close_speeds[]={0,0,0,20,40,60,80,100,120}`. Гейт медленно закрывается
после истечения таймера кнопки.
- Отрисовка: кадры гейта; у нас `draw_gate_back` в pop_bg (грань из левой комн.).
- **Проходимость**: Кид блокируется недостаточно открытым гейтом (коллизия
как стена, порог по высоте открытия); проходит при `modif` открытом.
---
## 3. Поправки к описанию пользователя (что важно)
1. Гейт НЕ в room6 (0,0) — он в **room8 (0,9)**, виден через левый шов; связь
кросс-комнатная (по таблице LINKLOC, не по позиции).
2. Кнопка может открывать несколько гейтов в разных комнатах.
3. Пики выдвигаются по `check_spike_below` (Кид над колонкой с пиками),
имеют состояния (не всегда смертельны), убираются со временем.
4. **Кросс-комнатное состояние тайлов ДОЛЖНО ПЕРСИСТИТЬ** — блокер (см. P0).
5. HP/смерть — новая подсистема.
6. Кнопка сама анимируется (нажата/отжата).
---
## 4. ПЛАН РЕАЛИЗАЦИИ (фазы)
### P0 — Персистентное per-room modifier-состояние + trob-каркас (ПРЕРЕКВИЗИТ)
**Проблема:** нажатие кнопки в room6 меняет modif гейта room8 (не текущей
комнаты); при входе в room8 нужно отрисовать гейт в текущем состоянии. Плюс
это чинит «re-entry восстанавливает тайлы» (loose/гейты).
Дизайн (предложение — уточнить в реализации):
- Массив `room_modif[24][30]` (или lazy per-visited-room) — modifier каждого
тайла каждой комнаты. Инициализируется из bg уровня при первой загрузке
комнаты; далее ЖИВЁТ (не перезагружается). ~720 Б — влезает в W2 (или в
EMM-страницу уровня рядом с данными: остаётся >10КБ).
- `enter_room` берёт modif из `room_modif[room]`, а fg — из level-страницы
(fg почти не меняется; исключения — loose→empty, надо тоже персистить: либо
отдельный `room_fg_override`, либо флаг «loose упал»).
- Обобщить loose-trob: единый список trob (room,tilepos,type) + диспетчер
`animate_*` по коду тайла. Loose (`pop_loose_*`) — перевести на него.
- Кросс-комнатный trigger: `add_trob` в НЕ текущую комнату меняет
`room_modif[room][tilepos]`; анимация продвигается даже для невидимой комнаты
(в оригинале — да; можно упростить: для невидимой комнаты гейт сразу в
финальном состоянии, анимировать только при видимости — решить при реализации).
### Фаза S — Пики (самодостаточно; отладит HP/смерть)
Порядок:
1. `room_modif` для пик (из P0 или временно локально).
2. `check_spike_below` (порт seg006:1199) — в `pop_phys_tick`.
3. `start_anim_spike` + `animate_spike` (порт seg007) — состояние в modif.
4. `is_spike_harmful` + `check_spiked` (порт seg006:0968).
5. **HP/смерть**: ввести `hitp_curr` (старт напр. 3); `take_hp`; при пиках —
мгновенная смерть; seq смерти (`seq_22_crushed`/`frame_177_spiked`..185);
анимация смерти; респавн (kid_init на старте или чек-поинт).
6. Отрисовка: кадры выдвижения пик. В pop_bg есть `SPIKES_FRAM_RIGHT` — нужны
pop-out кадры (spikes_fram по modif) + fore над Кидом.
### Фаза B — Кнопки + гейты (нужен P0)
Порядок:
1. Доступ к LINKLOC/LINKMAP из pop_level (добавить геттеры doorlink1/2[i] с
маппингом W0 или скопировать таблицы в W2 при load — 512 Б).
2. `pop_map` детект «Кид встал на кнопку» (check_press-путь) → `trigger_button`.
3. `trigger_button` → таймер + `do_trigger_list` (обход цепочки) →
`trigger_gate` для целей → изменить `room_modif[целевой]`.
4. `animate_button` (кнопка отжимается) + `animate_door` (гейт откр/закр +
авто-закрытие).
5. Отрисовка гейта (кадры по modif) через ЛЕВЫЙ шов (гейт room8 в room6) +
при входе в room8. Расширить `draw_gate_back`/добавить `draw_gate`.
6. Коллизия: закрытый гейт = стена (порог по высоте открытия); открытый —
проход. Учесть кросс-комнатный гейт на шве (проход влево room6→room8).
**Порядок фаз:** P0 → S → B. S в основном независим (кроме HP/trob-каркаса),
но проще и отладит смерть/анимацию; B требует P0 (кросс-комнатное состояние).
---
## 5. Открытые вопросы / грабли
- Персистентность `fg` для loose (тайл→empty): решить в P0 (override-массив или
флаг), иначе упавший loose «вернётся».
- Анимация trob в НЕВИДИМОЙ комнате: упростить (финальное состояние сразу) или
портировать честно.
- Двоебуфер: любой транзиент (пики/гейт/кнопка) финализировать перерисовкой
«покоя» на ОБЕИХ страницах (урок из loose — см. memory `pop_loose_floors`).
- Дверь уровня (lvldoor room9) + переход на level 2 — ОТДЕЛЬНО, отложено.
- L3-**вверх** (climb-up в комнату сверху) — ещё не сделан; можно закрыть до
объектов или параллельно.
+142
View File
@@ -0,0 +1,142 @@
# План: модульные тесты движка roomtest под ucsim_z80
Обвязка общая — `testkit/` в корне репозитория (там же объяснение, почему
прогон именно под z80, а не хостовым gcc). Наборы лежат в
`../roomtest/tests-host/`.
Задача плана: **перестать чинить одно и то же дважды**. За два прогона
уровня 1 (2026-08-03) закрыто восемь корней, и часть из них — регрессии
соседней механики, внесённые предыдущим фиксом. Такие вещи ловятся тестом
за миллисекунды, а в MAME — часами ручного вождения Кида.
## Что уже есть
| набор | модуль | статус |
|-------|--------|--------|
| `t_geom` | `pop_geom.c` | 39 проверок, включая побитовую сверку asm-LCG с 32-битной формулой на 128 шагах |
`pop_geom.c` выбран первым, потому что не тянет ничего за собой. Дальше
начинаются швы.
## Фаза 1. Два шва (блокирует всё остальное)
### 1.1 Доступ к странице уровня
`pop_level.c` ходит по абсолютным адресам: `gfx_w0_map(lvl_page)`, затем
разыменование `(uint8_t *)(LVL_DATA_OFF + …)`. В тестовом бинаре это
обращение в никуда.
Нужен макрос `W0PTR(off)`:
- на таргете — `((uint8_t *)(off))`, то есть ровно как сейчас;
- в тестах — смещение в обычном массиве-подложке.
Правка механическая и компайл-таймовая, на размер продукта не влияет.
Заодно снимает магию абсолютных констант из тела функций.
Тестовая подложка должна уметь: загрузить синтетическую комнату (10×3
байта fg + mod) и целый синтетический уровень на 24 комнаты, чтобы
проверять межкомнатные вещи.
### 1.2 Журналирующий рендерер
Вместо `pop_bg.c`/`pop_gdraw.c` в тестовый бинарь линкуется модуль с теми
же прототипами, который **не рисует, а записывает вызовы**: какой тайл
помечен к перерисовке, каким кодом, с каким счётчиком страниц.
Это не обход проблемы, а самостоятельная ценность: `BUG-GATE-ANIM-1` был
ровно такой формы — ворота меняли состояние, но пометка на перерисовку не
ставилась. Проверяется утверждением, а не глазами.
Минимум, который надо перехватывать: `pop_set_redraw`,
`pop_set_redraw_above`, `pop_loose_mob_spawn`, `pop_gate_redraw`.
## Фаза 2. Регрессионные кейсы из `bug_closed.md`
После швов `bug_closed.md` превращается в готовую спецификацию: у каждой
записи есть симптом и ожидаемое поведение. Кандидаты, которые ловятся
логикой (без отрисовки и без железа):
| баг | что закрепить тестом |
|-----|----------------------|
| `BUG-LVLSTATE-1` | запись тайла переживает выход из комнаты |
| `BUG-RESPAWN-1` | рестарт уровня возвращает ВСЕ тайлы из эталонной копии |
| `BUG-RESPAWN-2` | рестарт возвращает таблицу стражей; убитый снова жив |
| `BUG-GATE-ANIM-1` | смена состояния ворот ставит пометку `POP_RD_GATE`; закрывающиеся — на обе страницы, открывающиеся — на одну |
| `BUG-COLL-1` | `check_collisions` сканирует ряд справа налево и выбирает НАИМЕНЬШУЮ занятую колонку |
| `BUG-STANDUP-1` | `bumped_floor` у трупа (`alive >= 0`) только выравнивает и не трогает последовательность |
| `BUG-DEATH-1` | `hitp_curr == 0` при живом Киде переводит его в «умирает» ровно один раз |
| `BUG-LOOSE-2` | кусок, начавший падать, долетает и кладёт щебень ПОСЛЕ смены комнаты |
| `BUG-CEIL-2` | loose-плита ряда 2 верхнего соседа живёт как «ряд −1» |
`BUG-LOOSE-2` стоит взять первым: он до сих пор помечен в `bug_list.md`
как непроверенный именно потому, что гонку «уйти из комнаты раньше, чем
долетит плита» через мост MAME воспроизвести не удалось. На уровне логики
это несколько строк — заспавнить кусок, сменить комнату, тикать до
приземления, проверить щебень в данных уровня.
Не берутся (нужна картинка либо железо): `BUG-DOOR-CLIP`, `BUG-CEIL-1`,
`BUG-CEIL-3`, `BUG-OCCL-1`, `BUG-KBD-4`, `BUG-3`.
## Фаза 3. Сценарные тесты
Сейчас шаг кадра размазан по `main()` в `roomtest.c`. Вынести его в
`pop_frame_tick()` — тогда появляются тесты вида «поставить Кида в
известное состояние, скормить N тиков ввода, проверить итог»:
```
дано: комната 5, Кид на кнопке (0,6)
когда: 40 тиков без ввода
тогда: комната по-прежнему 5, Кид на полу ряда 2
```
Это тот самый BUG-STANDUP-1, который ловили потиковой трассой в MAME.
Ввод подаётся не через `kbd_raw_down()`, а через подменяемый источник —
это же даст возможность проигрывать записанные сценарии.
## Фаза 4. Дифф против SDLPoP
`SDLPoP/src/` лежит в дереве, собирается на хосте, и там **уже стоят
отладочные трассы** (`DBG kidobj tilepos=…` в seg008, `DBG make_loose_fall`
в seg007). Значит эталон можно заставить печатать потиковую трассу
автоматически.
Схема: общий формат скрипта ввода и общий формат трассы (тик, frame, x, y,
room, col, row, action, alive, hp). Гоняем обе реализации, диффим, первое
расхождение — номер тика и есть баг. Это ровно то, что делалось руками
через MAME, только бесплатно и повторяемо: `BUG-COLL-1` и `BUG-STANDUP-1`
такой дифф нашёл бы за секунды.
**Лицензия.** SDLPoP — GPLv3, правило подпроекта — читать и переписывать,
не линковать. Оракул обязан быть **отдельным исполняемым файлом**,
общающимся через файлы трасс, а не слинкованным с нашим кодом в один
бинарь.
Требование к детерминизму: сиды PRNG должны совпадать. У нас
`POP_PRANDOM_EXACT` даёт ту же последовательность, что в оригинале, и это
уже закреплено тестом `geom_lcg_matches_reference`.
## Чего эти тесты не поймают
Отрисовку, банки и W-окна, тайминги, клавиатуру — за этим остаётся MAME.
И отдельный класс: **баги порядка вызовов**. Свежий пример — окно
fore-клипа (`pop_fore_set_clip`) одно на всех, и его ставит каждый, кто
рисует персонажа; когда порядок «Кид/страж» стал переменным, окно осталось
стражьим, и Кид нарисовался поверх передних столбов. Это не «функция
вернула не то», unit-тест такое не видит. Ловится инвариантом,
вкомпилированным в safe-сборку: «в момент `pop_fore_over_kid` окно клипа
принадлежит Киду». Отдельный инструмент, дополняющий тесты.
## Порядок работ
1. Шов `W0PTR` + подложка уровня.
2. Журналирующий рендерер.
3. `BUG-LOOSE-2` — закрыть висящий вопрос.
4. Остальные кейсы из таблицы фазы 2.
5. `pop_frame_tick()` + сценарные тесты.
6. Дифф против SDLPoP.
Правило приёмки: тест не считается написанным, пока не проверен мутацией —
сломать проверяемое место и убедиться, что набор краснеет.
+91
View File
@@ -0,0 +1,91 @@
# Идеи и вопросы «на подумать» (PoP)
Не план работ, а список того, что осознанно отложено: каждая запись —
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
сделать это прямо сейчас.
## Зелье «переворот экрана» (upside-down)
**Вопрос пользователя (2026-08-01).** Тайлы фона у нас лежат строками, а
кадры Кида/стражей — КОЛОНКАМИ (`transpose_cols` в `pop_pack_kid.py`, ради
бесплатного горизонтального зеркала). Значит вертикальный переворот для
персонажей заметно сложнее, чем для фона. Верно; но прежде чем это чинить,
надо знать три факта.
**Факт 1 — когда оно вообще нужно.** Зелье переворота — тип 4
(`proc_get_object`, `seg006.c:1885``toggle_upside()`). Скан всех уровней
по данным (`res200N.bin`, тайл 10 = зелье, тип в backtable): тип 4
встречается **впервые на уровне 9** (две склянки), и больше нигде. Тип 3
(перо, медленное падение) — уровень 7. То есть **до уровня 9 механика не
нужна вообще**, и «на первом этапе просто не реализовывать» — не компромисс,
а точное соответствие данным уровней 1..8.
**Факт 2 — что именно делает оригинал.** НЕ переворачивает спрайты.
`flip_screen` (`seg009.c:1042`) → `flip_not_ega` (`seg009.c:1023`) меняет
местами СТРОКИ готового offscreen-буфера (top↔bottom, порядок пикселей
внутри строки не трогает — это вертикальное зеркало, не поворот на 180°).
Вызывается вокруг отрисовки кадра целиком (`seg003.c:296..301`): перевернул
буфер → дорисовал → перевернул обратно. Так что в оригинале это
post-process всего экрана, и вопрос «как перевернуть колоночный спрайт»
там просто не возникает.
**Факт 3 — почему нам этот приём не подходит как есть.** У нас нет шага
«готовый offscreen → экран»: рисуем прямо в видеостраницу, а heal берёт фон
из ОЗУ-копии этой же страницы. Переворот всей страницы построчно — это
320×192 Б копирования КАЖДЫЙ кадр, что мимо бюджета на порядок.
**Варианты, которые надо будет взвесить (не сейчас):**
1. **Предпечённые перевёрнутые атласы.** Второй набор кадров
Кида/стража, перевёрнутый по вертикали ещё в `pop_pack_kid.py` (там уже
есть транспонирование — добавляется одной строкой). Рантайм: выбор
набора + зеркальная арифметика Y. Память: ещё ~28 страниц EMM при
бюджете ~3.3 МБ — не проблема. Похоже, самый дешёвый по тактам путь.
2. **Фон рисовать с обратным Y** — для row-major тайлов строка остаётся
непрерывным accel-прогоном, меняется только адрес назначения; цена —
вызов на строку вместо вызова на тайл. Померить, прежде чем закладывать.
3. **Аппаратная помощь** — до проектирования проверить, есть ли у
акселератора направление копирования «вниз» (обратный инкремент адреса);
если есть, вариант 1 может и не понадобиться. Смотреть
`docs/new/06-accel.md` и `docs/reference/accel_r.txt`.
**Почему не сейчас.** Уровни 1..8 этого не требуют, а к уровню 9 у нас уже
будет ответ на вопрос «сколько стоит кадр» (задачи CLIP-1/T-2) — без него
выбирать между вариантами выше бессмысленно.
## Заменить генератор псевдослучайных чисел
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
## Отключать мышь на время игры
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
см. memory `mouse_api`). Игре она не нужна вообще: управление —
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
который у нас и без того занят на 86 %.
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
кадров подряд прошли без единого превышения. То есть эффект реальный и
измеримый, просто в тесте он был наведён извне.
**Что проверить.**
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
«hide cursor» и снимает ли она обработчик.
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
3. Не ломает ли отключение выход в DSS: состояние обязано
восстанавливаться при `exit`, включая аварийный (atexit).
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
после выхода — заметный. Делать после того, как закроем стражей и
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
возможный возврат `pop_bg` в резидент `--w3`).
+513
View File
@@ -0,0 +1,513 @@
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
> **Замер 2026-08-01 (актуальная сборка).** `_CODE` 25 119 Б, `_DATA` 3 709,
> куча ~2.4 КБ. Банки: 1 (`guards.c`) 1 896, 2 (`pop_bg.c`) 13 792,
> 3 (`pop_map.c`) 6 331, 4 (`pop_gdraw.c`) 2 236 — все из 16 384.
> **Резидента `--w3` больше нет**: отрисовка уехала в банк 2, и это сняло
> главное ограничение резидента (из банка его было не достать) — банк→банк
> работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража
> вынесена из банка 2 в собственный банк 4, потому что банк 2 подошёл к
> потолку (16 021 из 16 384) — коммит `2f3e854`.
>
> **Свободного места в банке 2 осталось ~2.6 КБ**, а туда же просятся
> чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать
> `levels_plan.md` §3 — посчитать, куда это ляжет. Свободные номера банков
> есть, гранулярность — файл.
>
> Из плана ниже **не сделаны шаги 5 (данные: `room_modif`, `dl1/dl2`,
> `_kbdraw_down`; потенциал ~1.5 КБ) и 7 (дедуп `draw_tile`, отложен по
> решению пользователя)**.
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
как полностью перекрытый этим документом. Документ самодостаточный
— рассчитан на старт с пустого контекста.
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
ограничения окна W3.
---
## СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)
| Шаг | Статус | Факт |
|-----|--------|------|
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | 41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
| 4. Разгрузка/перебалансировка W3 | **сделан** | резидент = `pop_bg` + `pop_gdraw` (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б) |
| 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
**Замер сейчас против замера §1:** `_CODE` 24 881 → 24 421, куча W2 2 076 → 2 592 Б,
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
**Замер 2026-07-29 (после стража).** Появление стража съело кучу до 805 Б;
разгрузка — вынос ОТРИСОВКИ стража в резидент (`pop_gdraw.c`, `--w3`), логика
и состояние остались в W1/W2, чтобы банк `guards.c` их видел (R2). Итог:
`_CODE` 26 149 → 25 703, куча **805 → 1 245 Б**, W3-резидент 11 656 → 12 819
(свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент»
теперь формулируется одним правилом: **резидент = только то, что рисует и
зовётся исключительно из главного цикла**; всё, что может понадобиться банку,
остаётся в W1/W2.
### Что сделано вместо §5.3 (вынос loose в W3)
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
**порт архитектуры оригинала**: логика ставит пометку, отрисовка идёт
отдельным проходом — `set_redraw_*` (seg007) + `redraw_needed` (seg008:0178).
Появился `pop_redraw.c/.h`; `pop_trob` и `pop_map` больше не рисуют тайлы.
Наши самодельные счётчики (`spike_rest`, `button_rest`, `ldoor_rest`,
`loose_bake`, `loose_rest`, `ceil_rest`, `ceil_bake`, `land_bake`) удалены —
их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.
**Два исключения остались** (обе — функции ТОЛЬКО главного цикла, звать из
банка нельзя):
- `pop_process_trobs` — пламя факела и пузырёк зелья (покадровый оверлей);
- `pop_loose_tick` — падающий кусок (mob): spawn/tick/pos. В оригинале это
отдельная подсистема (`mobs` + `draw_moving`), разделение на логику и
отрисовку — задел следующей фазы.
### Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА
`tests/w3bankgfx` (huge + `--w3 res.c` + `--bank 1=bank1.c`, графика 256):
- банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри
банка до блита, после блита и после возврата из вызванной им W1/W2-функции
одна и та же (0xF0), резидент — 0xF3. То есть `_bgi_begin`/`_bgi_end`
корректно возвращают ИМЕННО банковую страницу (правило R4);
- вызов W1/W2-функции из банка работает, и она тоже может рисовать;
- резидент W3 жив и вызывается после возврата из банка (R3).
**Дополнительно выяснено (важно для стражей):** писучие статики
`__banked`-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не
прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё
состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в
W1/W2 как обычные глобалы, а банк — только код.
Ещё одна мина, найденная там же: инлайновый `in a,(#0xE2)` посреди тела
функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет
заливки стал номером страницы, и «резидент не рисовал»). Читать порт
отдельной `__naked`-функцией.
### Найденная по дороге ловушка компилятора
`(const T *)КОНСТАНТА + var*K` SDCC 4.5 может собрать неверно: умножение
делает в 16 битах, а потом берёт только младший байт (`ld c,l` / `inc b`).
Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага
пропадал `FRAME_NEEDS_FLOOR` и персонаж проваливался сквозь пол. Лечение —
считать адрес в `uint16_t` и кастовать один раз. Тот же паттерн в
`pop_level.c` компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа
работает» нельзя. Подробности: memory `sdcc_z80_const_ptr_index_bug`.
---
## 0. Что уже сделано из v1 (не повторять)
- `--opt-code-size` и `--max-allocs 100000` **уже включены по умолчанию** в
`bin/sprinter-cc` (v1 §2.1 закрыт, выигрыш получен).
- Лишние блиты переднего слоя убраны (v1 §7 п.0): `fore_tile` больше не рисует
`bottom_id`, `_CODE` 388 Б.
- `gfx_blit_noclip` в libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.
- `--w3` как резидент окна 3 реализован и обкатан (memory `w3_resident_code`).
---
## 1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)
Команда: `--memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c`.
### 1.1 Окна
| Область | Занято | Свободно | Примечание |
|---|---|---|---|
| W1+W2 `_CODE` | 24 881 Б | — | 0x4100…0xA231 |
| W1+W2 `_HOME`+`_GSINIT`+`_DATA`+`_BSS` | ~4 240 Б | — | до 0xB2E4 |
| **W1+W2 куча** | 0 (никто не malloc'ит) | **2 076 Б** | 0xB2E4…0xBB00 |
| W1+W2 стек | — | 1 279 Б | 0xBB00…0xBFFE |
| **W3 резидент** | 14 376 Б | **2 008 Б** | 0xC000…0xF828 |
| EMM-страницы | 37 атласов + 1 уровень | ~215 страниц свободно | `sprinter_emm_budget` |
**Итого запаса до стены: ≈ 4 КБ** (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда
не влезут.
### 1.2 Код по модулям (точно, из `.rel`)
```
W1/W2 (_CODE 24 881): W3 резидент (_W3CODE 14 376):
pop_kid 6 963 pop_bg 11 643
pop_map 6 157 pop_trob 2 733
roomtest 2 583
pop_level 1 425
pop_ctrl 1 145
crt0 333
libc+libbgi ~6 275
```
### 1.3 Крупнейшие функции/данные (из `.lst`)
```
pop_bg : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
fore_only_tile 409, climb_overlay_tile 391, tile_table 371
pop_map : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
get_tile 266, do_knock 243, check_press 238, check_leave 212
pop_kid : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
(в _CODE!), собственно кода ~3.2 КБ
roomtest : enter_room+main ~2.1 КБ
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)
```
### 1.4 `_DATA` (3 710 Б)
```
pop_trob 963 (room_modif[24][30] = 720 + trobs + rest-массивы)
pop_level 515 (копии LINKLOC/LINKMAP уровня)
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200
```
### 1.5 Находки замера (мелкие, но чинить)
1. **`--w3` берёт ОДИН файл на флаг.** В `Makefile` написано
`--w3 pop_trob.c pop_map.c --w3 pop_bg.c`, и это значит «W3 = pop_trob и
pop_bg», а `pop_map.c` компилируется как обычный исходник в W1/W2. Судя по
`.sprinter-cc-roomtest/w3_pop_map.rel` (устаревший артефакт), когда-то
pop_map был в W3. **Решить осознанно** (см. §4) и записать явно:
`--w3 pop_trob.c --w3 pop_bg.c`.
2. `libc` тянет `_irq_state` 818 Б + `_kbdraw_state` 515 Б в `_DATA`.
`__irq_vec_buf` (513 Б) — таблица векторов IM2; `__kbdraw_down` (512 Б) —
битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ
в самом дефицитном окне.
---
## 2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА
Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по
`bin/sprinter-cc` (справка `--w3`/`--bank`) и по коду libbgi.
**(R1) Резидент W3 (`--w3`) и банки W3 (`--bank`) делят одно окно.**
Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова
`__banked` подменяет страницу W3 на банковую и возвращает резидентную назад.
**(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже.**
Пока исполняется банк, резидентной страницы в адресном пространстве нет.
Значит нельзя не только `bank → pop_bg()`, но и `bank → pop_map() → pop_bg()`.
**Это ключевое ограничение при выборе, что делать банком.**
**(R3) Резидент → банк работает** (через трамплин в W1), резидент → W1/W2 —
тоже.
**(R4) Графические примитивы libbgi звать можно откуда угодно.**
`_bgi_begin` читает текущую страницу W3 из порта 0xE2, а `_bgi_end` её
возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя
только одно: **звать `_bgi_begin`/`_bgi_end` ИЗ кода, который сам лежит в W3**
(после подмены страницы исчезнет исполняемый код — проверено, белый экран).
Поэтому `pop_bg` (резидент W3) обязан пользоваться готовыми примитивами
(`gfx_blit*`, `bar`, …), а батчинг скобки на весь `draw_tile` (v1 §8 шаг 1)
для него **невозможен** без переноса самого `draw_tile` в W1/W2.
**(R5) `--w3` кладёт в W3 код И rodata модуля** (`--codeseg/--constseg
W3CODE`), а писучие статики оставляет в `_DATA` (W2). То есть `const`-таблицы
переносятся в W3 бесплатно вместе с модулем (так уже лежит `tile_table` 371 Б).
**(R6) Данные в EMM-странице читаются, только пока страница в окне.**
`gfx_w0_map(page)` / `gfx_w0_unmap()` — окно W0 (0x0000…0x3FFF), первые 0x100
занимает ISR-стаб. Так уже работает `pop_level`. Цена — пара `OUT` на
маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем
цикле.
---
## 3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)
- **Нельзя банковать `pop_bg`.** Он вызывается из pop_map, pop_trob, roomtest,
pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит
`tile_table` и всю отрисовку. Банк дал бы трамплин на каждый блит.
- **Нельзя банковать `pop_map`, пока `pop_map` зовёт `pop_bg`** (R2). Сейчас
зовёт: `pop_loose_tick` и компания (~30 вызовов графики).
- **Нельзя тащить `kid_frames` в EMM «в лоб»**: он читается несколько раз за
кадр из коллизии (`kid_cur_dx`/`kid_cur_flags``dx_weight`,
`char_x_forward_edge`, …). Нужен кэш кадра (см. §5.1) — иначе маппинг
страницы окажется в горячем пути.
- **Нельзя «причёсывать» семейство `draw_tile` ради экономии, не имея
пиксельного теста.** Мы неделю выравнивали слои по SDLPoP; любой рефактор
этой зоны проверять диффом страниц (заморозка кадра клавишей `1` + сравнение
VRAM обеих страниц, приём из memory `mame_mcp_bridge`).
---
## 4. ЦЕЛЕВАЯ РАСКЛАДКА
Принцип: **W3-резидент = «толстая графика, которую зовёт только главный цикл»;
W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки =
новая холодная логика (стражи, боёвка, будущие уровни)**.
```
W1/W2 (всегда отображено) W3 резидент (стр. 0xC000) Банки W3
────────────────────────── ───────────────────────── ─────────
libc + libbgi pop_bg (отрисовка тайлов) guards.c
pop_kid (интерпретатор+рисование) pop_trob (анимации тайлов) fight.c
pop_map (коллизия/физика/предметы) pop_loose.c (loose+потолок) debug/roomnav
pop_geom (общая геометрия/тайлы) enter_room-часть roomtest?
pop_ctrl (ввод/диспетчер)
roomtest (главный цикл)
```
Почему так:
- **`pop_map` остаётся в W1/W2** — его зовут и главный цикл, и (в будущем)
банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо
вынести графическую часть (loose/потолок) — она уезжает в W3 к `pop_bg`
(§5.3). После выноса `pop_map` становится **чистой логикой без единого
вызова графики** — тот самый bank-safe API.
- **`pop_kid` остаётся в W1/W2**: `play_seq`/`kid_set_seq`/`Kid` нужны и
стражам (у стражей ТА ЖЕ seqtbl), а `kid_draw` зовёт только libbgi (R4).
- **`pop_trob` остаётся резидентом**: его зовёт только главный цикл, и он сам
зовёт `pop_bg` — идеальный житель W3.
- **Банк стражей не зовёт ничего из W3.** Рисование стражей — либо через
libbgi напрямую (R4), либо (лучше) резидентный `guard_draw()` в W1/W2 рядом
с `kid_draw`, а банк только считает состояние. Тот же приём мы уже
используем для `pop_item_taken`/`pop_loose_fell`/`pop_ceil_fell`: банк
выставляет флаг — резидент рисует.
---
## 5. ПЛАН РАБОТ
Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место
следующему.
### Шаг 1. `kid_data.h` (3 745 Б) → EMM-страница + порт `load_frame` — **самый большой выигрыш**
Сейчас `kid_seqtbl` (2310) + `kid_frames` (1205) + `kid_seq_off` (230) лежат в
`_CODE` окна W1/W2 — это 15 % всего дефицитного пространства.
Как переносить:
1. `pop_extract_kid_data.py` дополнительно пишет `kid_data.bin` (те же три
таблицы подряд, фиксированные смещения).
2. Грузим её в отдельную EMM-страницу тем же способом, что уровень
(`pop_level_load` — готовый образец), хэндл держим в `pop_kid`.
3. **Порт `load_frame` (seg006) и глобала `cur_frame`** — в оригинале ровно
так и сделано: раз за тик кадр копируется в структуру, а весь остальной код
читает `cur_frame`, а не таблицу. У нас `kid_cur_dx()/kid_cur_flags()`
станут чтением из `cur_frame` (5 байт в `_DATA`).
4. `play_seq` оборачивается в один `gfx_w0_map(kid_data_page)``unmap` на
вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).
Выигрыш: **3 745 Б из W1/W2**, цена — один маппинг страницы за тик и 5 байт
`_DATA`. Дополнительный бонус: `load_frame`/`cur_frame` — шаг К СХОДСТВУ с
оригиналом, а не отход от него.
Риск: сломать `play_seq` (сердце анимации). Проверка: прогон по комнатам с
эталонными позами (вис, подтягивание, прыжки, подъём меча).
### Шаг 2. Модуль `pop_geom.c` — дедуп + bank-safe фундамент
Сейчас продублировано между модулями:
| что | где | сколько |
|---|---|---|
| `y_to_row`/`y_to_row_mod4` | pop_bg + pop_map | 2 копии |
| `char_dx_forward` | pop_kid + pop_map | 2 копии |
| `x_bump[20]` | pop_kid (uint8) + pop_map (int16) | 20 + 40 Б, РАЗНЫЕ типы |
| `y_land[5]` | pop_kid + pop_map | 10 + 10 Б |
| `tile_is_floor` | pop_map (+ проверка кодов в roomtest) | 2 места |
| 32-битный LCG `prandom` | pop_bg (`prandom`) + pop_trob (`trob_prandom`) | 2 копии по ~60 Б + 2 сида |
Собрать в один W1/W2-модуль `pop_geom.c`: таблицы `x_bump/y_land/dir_front/
dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
`tile_is_floor`, `prandom`. Выигрыш прямой — сотни байт (оценка 250–400 Б),
но главное — **это и есть тот «чистый» API, который потом сможет звать банк**
(R2): вся геометрия оказывается в W1/W2 по определению.
Осторожно: `prandom` у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными
сидами (стены vs фазы факелов). Объединять функцию можно, **сиды — нет**:
передавать сид указателем/по индексу, иначе поедет раскладка кладки.
### Шаг 3. Вынести loose/потолок из `pop_map` в W3
`pop_map` — единственный модуль W1/W2, который зовёт графику, и делает это
ровно в одном логическом блоке: `pop_loose_tick` + `check_press` + `do_knock` +
`fell_on_your_head` + `check_loose_fall_on_kid` + плита-потолок (~1.4–2 КБ).
Вынести их в `pop_loose.c`, собираемый `--w3` рядом с `pop_bg`/`pop_trob`.
Тогда:
- `pop_map` = чистая логика (bank-safe, R2 соблюдён);
- W1/W2 худеет ещё на ~1.5–2 КБ;
- W3 растёт на столько же — а место там появится после шага 4.
### Шаг 4. Перебалансировка резидента W3
После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:
1. **`wall_pattern` (944 Б) + `mob_render`/`mob_tick_one` (1381 Б)** — кандидаты
на переезд в W1/W2: их зовёт только `pop_bg`/`pop_loose`, но сами они уже
пользуются только libbgi (R4), значит из W1/W2 работают и остаются
достижимыми из банка.
2. `tile_table` и мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в
EMM-страницу **уровня** (там ~13.8 КБ свободно) — но только если чтение
происходит под уже замапленной страницей. Сейчас `draw_tile` читает
`tile_table` ВНЕ W0-контекста → потребуется явный маппинг на тайл. **Не
делать раньше замера**: 30 тайлов на входе в комнату × map/unmap — терпимо,
а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.
3. Если и этого мало — `enter_room` (~2.1 КБ, зовётся только при смене комнаты)
переносится в резидент W3 или в БАНК (он вызывается из главного цикла =
резидента, значит банк допустим по R3).
### Шаг 5. Данные
1. **`room_modif[24][30]` = 720 Б** (pop_trob, `_DATA`). Нужен произвольный
доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты ×
30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей
комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице
уровня, а в `_DATA` держать кэш на 2–3 комнаты (свою + левого/правого
соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена —
аккуратность на швах (кнопка в одной комнате открывает ворота в другой).
**Делать последним** — это самая «тонкая» правка по семантике.
2. **`dl1[256]`+`dl2[256]` = 512 Б** (pop_level, `_DATA` — копии LINKLOC/
LINKMAP уровня) — читаются при нажатии кнопки
и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы
уровня (она и так маппится) — но проверить, что `pop_doorlink2` не зовётся
из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б.
3. **`_kbdraw_down[512]` 512 Б** (libc): проверено — это **байт на скан-код**
(`libc/kbd/_kbdraw_state.c`), хотя комментарий называет его битовой картой.
Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и в
`kbd_raw_down`. Трогать осторожно: raw-клавиатура уже дважды была
источником залипаний (memory `kbd_raw_fifo_drain`,
`kbd_overrun_wipe_modifiers`) — правку сопровождать прогоном docs/kbd-games.
4. **`__irq_vec_buf` 513 Б** (libc IM2): таблица векторов обязана быть
выровнена и полна — не трогать.
### Шаг 6. Контракт банка стражей (проектируется ДО написания кода)
Когда дойдём до стражей:
- `guards.c` собирается `--bank 1=guards.c`, режим `huge` (или `big` с
`BANKED=W1`, если W3 окажется тесен для трамплинов).
- **Банк зовёт только:** `pop_map` (чистая логика после шага 3), `pop_kid`
(`play_seq`, `kid_set_seq`, `cur_frame`), `pop_geom`, libc/libbgi.
- **Банк НЕ зовёт:** `pop_bg`, `pop_trob`, `pop_loose` (резидент W3) — ни
прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг,
рисует резидент (идиома `pop_item_taken`).
- Первым делом — **пробник** (`tests/` или `--bank` на пустышке): банк зовёт
`pop_map`-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка
W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не
веры в неё.
### Шаг 7. Мелкий дедуп в `pop_bg` (после того, как появится тест страниц)
- Пять функций-редроев (`pop_spike_redraw`, `pop_loose_shake_draw`,
`pop_floor_bake`, `pop_button_redraw`, `pop_leveldoor_redraw`) отличаются
только прямоугольником heal, банком и набором тайлов — свести к одному
параметризованному хелперу (оценка −150…250 Б).
- `env_b/wall_b/fore_b/pot_b` — четыре одинаковых обёртки над `blit_b`
(оставить: экономия единицы байт, читаемость дороже).
- `overlay_mid_tile` / `fore_only_tile` / `climb_overlay_tile` / `draw_tile`
— общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои».
Тут экономия потенциально сотни байт, но это **та самая зона риска из §3**
только с пиксельным диффом до/после и по одному слою за раз.
---
## 6. Ожидаемый итог
| Шаг | W1/W2 | W3 | Риск |
|---|---|---|---|
| 1. kid_data → EMM + load_frame | **3 745** | — | средний (сердце анимации) |
| 2. pop_geom (дедуп) | 250…400 | — | низкий |
| 3. loose → W3 | 1 500…2 000 | +1 500…2 000 | низкий (перенос как есть) |
| 4. разгрузка W3 (wall_pattern, mob) | +2 300 | 2 300 | низкий |
| 5. данные (room_modif, LINKLOC, kbd) | 1 000…1 600 | — | средний/высокий |
| 7. дедуп редроев pop_bg | — | −150…250 | средний |
Суммарно: **W1/W2 освобождается ~4.5–6 КБ**, W3 остаётся примерно в нынешнем
объёме, но становится «правильно заполненным» — в нём только то, что банк
никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают
до 16 КБ на банк, не трогая резидент.
---
## 7. Как мерить и проверять (обязательно к каждому шагу)
1. **До/после по `.rel`** — точные размеры на модуль:
`for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done`
(области `_CODE`/`_W3CODE`/`_DATA`). Итоги окон печатает сам `sprinter-cc`.
2. **Функции** — из `.lst` (метки `_name:` и адреса), скрипт в истории этой
сессии; полезно ловить «функция распухла после рефактора».
3. **MAME**: любой перенос кода между окнами/страницами — это класс «молча
ломается» (`sprinter_memory_modes`). Минимум: комната 1 (loose), 12
(вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота).
4. **Пиксельный дифф** для правок отрисовки: заморозить кадр (`1`), сравнить
обе страницы дабл-буфера через `vram` (см. memory `mame_mcp_bridge`) и/или
сверить с эталонным рендером `render_room.py`.
5. **Скорость** — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE
(приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.
## 8. Ссылки
- `bin/sprinter-cc` — справка по `--w3`, `--bank`, `--memory`, `--memory-manual`.
- `runtime/crt0_banked.s`, `runtime/bank.s` — трамплины и захват резидентной
страницы W3.
- `libbgi/common/_bgi_begin.c` / `_bgi_end.c` — механика скобки W3 (R4).
- memory: `w3_resident_code`, `pop_banking_architecture`, `sdcc_banking`,
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
`libc_one_function_per_module`.
- `applications/PoP/roomtest/TASKS.md` — что из этого берётся в работу сейчас.
---
## 9. Скорость отрисовки: замеры и запас
Перенесено из удалённого `size_optimization_plan.md` §8 (замер 2026-07-27) —
единственная его часть, которая не была перекрыта этим документом.
Профилирование в MAME: маркеры в порт 0xFE + `wpiset … totalcycles` (приём из
memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже стоят полосы
бордюра `PROF()`. Кадр Sprinter = **430 080 тактов**.
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
пересчёт src, нарезка полос >256), а не за пиксели:
| путь (спрайт 32×3) | тактов |
|---|---|
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
| линейное ядро без клипа | 4 617 |
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
кадра**; сам `bar` — только 13 308.
**Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на
него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже
тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`).
**Не закрыт heal** — задача CLIP-1 в `../roomtest/TASKS.md`.
**ВАЖНО:** W3-скобку (`_bgi_begin`/`_bgi_end`) ставит САМА libbgi — вызывать
её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
белый экран).
**Запас, когда перестанет хватать бюджета кадра:**
1. **Батчинг W3-скобки** — одна `_bgi_begin`/`_bgi_end` на весь `draw_tile`
вместо скобки на блит; нужен публичный batch-API в libbgi.
**Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`,
длинная серия задержит кадровое прерывание — а по разбору KBD-1
(`../roomtest/TASKS.md`) длинные DI-окна и есть причина потери байт
клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую.
2. **Решётка ворот одним спрайтом**`draw_gate_back` рисует бары по одному
(до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
`gfx_blit_part` с обрезкой по фазе `gate_bot_y & 7`: 7 блитов → 1.
3. **Не перерисовывать статичные части шва** — грань ворот, пол и кромка при
анимации решётки не меняются (см. OPT-1 в `../roomtest/bug_closed.md`
решено не делать, стоимость транзиентная).
4. **T-1 / T-2** (`../roomtest/bug_list.md`) — перерисовка пик по причине и
idle-skip Кида: самый большой оставшийся резерв, потому что убирает работу
целиком, а не удешевляет её.
+197
View File
@@ -0,0 +1,197 @@
# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты)
Статус: план, 2026-08-01. Продолжает `PORT_PLAN.md` §7 (Фаза 1: «переходы
между экранами» → теперь между УРОВНЯМИ). Текущая точка: `roomtest` играет
уровень 1 целиком в одной комнате-за-комнатой модели, но уровень нельзя
ни выбрать, ни закончить.
Источник истины — `../SDLPoP/src/` (правило `../CLAUDE.md`). Ключевые
места: `seg000.c: load_lev_spr/play_level_2/init_game`, `seg005.c:
up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`,
`seg002.c` (спецсобытия уровней), `data.h:835..850` (потабличные различия
уровней).
---
## 0. Что уже готово (не проектировать заново)
- **Формат и загрузчик уровня.** `pop_level.c/.h` читает сырой
`res200N.bin` (2305 Б) в отдельную EMM-страницу; путь — параметр
`pop_level_load(const char *)`. Мультиуровневость здесь стоит одной
функции формирования имени.
- **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`,
`pop_level_start_pos()`, `pop_level_start_dir()` — реализованы и пока
НЕ вызываются (см. `../roomtest/TASKS.md` L1-START).
- **Страж по данным уровня**: `pop_level_guard()` (порт `enter_guard`),
сохранение состояния между комнатами (`pop_guard_state_save`).
- **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py`
`build_palette`): env 0x50, wall 0x60, pot 0x40, kid 0x70, меч 0x80,
страж 0x90. Это тот же раскрой, что `set_pal_arr(0x50/0x60)` в
`seg000.c:1140..1148`, — значит смена тайлсета не требует переиндексации
спрайтов Кида (см. §3).
- **Все 16 файлов уровней распакованы**: `../SDLPoP/data/LEVELS/res2000..
res2015.bin` (0 — демо-уровень).
---
## 1. Что реально различается между уровнями (замер по данным, не по памяти)
Таблицы из `../SDLPoP/src/data.h:840..847` + инвентарь тайлов, снятый прямо
с `res200N.bin` (маска `fg & 0x1F`):
| Ур. | Тайлсет | Страж | Новое против предыдущих |
|-----|---------|-------|--------------------------|
| 1 | dungeon | обычный | — (текущая база) |
| **2** | **dungeon** | **обычный** | **ничего нового: тот же набор объектов минус меч** |
| 3 | dungeon | СКЕЛЕТ | чомперы |
| 4 | palace | обычный | **тайлсет palace**, зеркало (спецсобытие `mirror_level`) |
| 5 | palace | обычный | — |
| 6 | palace | ТОЛСТЫЙ | падение на входе (спецсобытие) |
| 7 | dungeon | обычный | — |
| 8, 9 | dungeon | обычный | — |
| 10, 11 | palace | обычный | — |
| 12 | dungeon | ТЕНЬ | seamless-выход (комната 23), исчезающий меч |
| 13 | dungeon | ВИЗИРЬ | мышь, особый выход |
| 14 | palace | нет | — |
| 15 | dungeon | нет | финал |
Прямое следствие для порядка работ: **уровень 2 не требует ни одного нового
ассета и ни одной новой механики** — он проверяет ровно машинерию перехода.
Это и есть первый шаг.
Прочие потабличные различия, которые придётся завести массивами по 16:
`tbl_level_type` (тайлсет), `tbl_guard_type` (−1 = стражей нет),
`tbl_guard_hp`, `tbl_level_color` (вариантные палитры, 1.3), `tbl_entry_pose`.
---
## 2. Шаг 1 — машинерия перехода (цель: уровни 1 → 2 → 3) — **СДЕЛАН 2026-08-04**
> **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 →
> Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие
> колонны рисуются) → уровень 3. Разбор что именно сделано и что по
> уровню 2 осталось — `../roomtest/TASKS.md`, запись **L2**.
>
> Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется:
> **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина
> двери уровня) и **большую склянку** `add_life` (тип зелья 2, комната 20).
Порядок именно такой; каждый пункт проверяем в MAME отдельно.
**2.1 Выход с уровня.** Портировать `up_pressed()` ветку двери
(`seg005.c:410..423`) + `go_up_leveldoor()` (`seg005.c:497`): дверь рядом
(при/за/перед персонажем) И `drawn_room != level.start_room` И створка
открыта полностью (`curr_room_modif >= 42` — вариант `fix_exit_door`) →
`Char.x = x_bump[...] + 10`, направление влево, `seq_70_go_up_on_level_door`.
Затем оживить опкод `0xF1 END_LEVEL` в `play_seq` (`../roomtest/pop_kid.c:418`
— сейчас пустой `break`): `++pop_next_level`, как `seg006.c:662`.
**2.2 Цикл уровня.** В `main()` после тика: `if (pop_next_level !=
pop_current_level) → load_level(pop_next_level)`. Порядок сноса/подъёма
состояния (порт `load_lev_spr` + `play_level_2`):
`pop_level_free` → `pop_level_load("LEVELS\\res200%d.bin")` →
`pop_trob_reset` → `pop_guard_reset` → сброс tile-override'ов
(`ovr_*` в `roomtest.c`) → `enter_room(pop_level_start_room())` →
`kid_init(поза/позиция/направление из данных уровня)`.
**HP через уровень переносится** (в оригинале `hitp_beg_lev`), не сбрасывать
в максимум — сверить с `seg000.c` `init_game`/`play_level_2`.
**2.3 Стражи по уровню.** Завести `tbl_guard_type[16]`/`tbl_guard_hp[16]`;
`-1` = стражей на уровне нет (уровни 14, 15) — `pop_guard_enter` обязан это
понимать, иначе на 14-м полезут стражи из мусора. Для шага 1 (уровни 2, 3)
достаточно обычного стража, но проверку `-1` заложить сразу.
**2.4 Чит «следующий уровень» (Shift+L).** Реализуется ровно тем же
`pop_next_level` — и без него отладка уровней превращается в прохождение
игры руками. Делать в этом же шаге, не позже (см. §4).
**Приёмка шага 1:** дверь уровня 1 → уровень 2 играется целиком → его дверь
→ уровень 3 стартует (чомперы могут быть ещё не портированы — тогда
фиксируем как известное ограничение, а не «баг»).
---
## 3. Шаг 2 — второй тайлсет (palace, уровни 4+)
**Ассеты.** `toolchain/pop_pack_bg.py` уже читает PNG каскадом
VDUNGEON→VPALACE (та же логика, что в игре), но печёт ОДИН набор атласов
(`pop_env0..4.atl`, `pop_wall.atl`, `pop_fore.atl` ≈ 75 КБ). Нужен второй
набор из VPALACE (`pal_env*.atl` / `pal_wall.atl`), плюс `torch_debris` —
тайл, который встречается только на palace-уровнях. По EMM это ещё ~6
страниц при бюджете ~3.3 МБ — не проблема.
**Палитра — главный технический вопрос, и он уже решён раскроем.**
Тайлсет живёт в слотах `0x50..0x5F` (env) и `0x60..0x6F` (wall); Кид, меч,
страж, склянки — в других слотах. Значит смена тайлсета = перезапись 32
записей палитры (`gfx_pal_set` на обе страницы, как `flash_bg` в
`roomtest.c`), а НЕ перезагрузка `kid.pal` и не переиндексация спрайтов.
Сделать `pal_dungeon.bin` / `pal_palace.bin` (по 32 записи) и грузить при
смене типа уровня. Проверить артефактом: скриншот palace-комнаты против
рендера `render_room.py` для того же уровня.
**Вариантные цвета уровней** (`tbl_level_color`, `level_var_palettes` — это
уже 1.3, в 1.0 их нет): по той же механике, тот же диапазон слотов. Решение
на будущее — сначала базовые два тайлсета, потом при желании цвета.
**Выбор набора в коде.** `pop_bg_load()` сейчас грузит фиксированные имена;
превратить в `pop_bg_load(type)` с двумя таблицами имён + выгрузка старых
атласов при смене типа (`atlas_free`). Переключение — только на границе
уровня, не в кадре.
---
## 4. Читы SDLPoP: что взять на следующем этапе
Из `../SDLPoP/README.md` (раздел Cheats). У нас уже есть: **K** — убить
стража, **I** — бессмертие (наш, в оригинале нет), **S** — выдать меч (наш),
**+/−** — обход комнат (`ROOMNAV`, наш).
**Брать сразу вместе с переходами уровней** (без них отладка дороже самой
работы):
| Чит | Что даёт | Цена |
|-----|----------|------|
| **Shift+L — следующий уровень** | единственный вменяемый способ тестировать уровни 2..15 | тривиально: `++pop_next_level` из §2.2 |
| **R — воскресить Кида** | у нас респавн по ↑ + таймаут; порт `resurrect` ближе к оригиналу и не мешает управлению | низкая |
| **Shift+S / Shift+T — +1 HP / +максимум** | отладка боёвки без «ровно трёх попыток»; честная замена нашему читу бессмертия | низкая, HP-машинерия уже есть |
| **[ и ] — сдвинуть Кида на пиксель** (debug-чит SDLPoP) | прямо бьёт в наш класс багов «окклюзия/шов на один пиксель» — воспроизведение позы без ловли момента | тривиально |
**Брать во вторую очередь:**
| Чит | Почему позже |
|-----|--------------|
| **H / J / U / N + Ctrl+B — смотреть соседние комнаты** | требует честной модели `drawn_room ≠ Kid.room` (наш S3-straddle, каркас есть: `update_kid_render_dx`). Зато потом заменяет самодельный `ROOMNAV` и попутно закрывает straddle-задачу |
| **Shift+W — медленное падение (feather)** | ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` уже есть, но не проверена ничем — чит станет её единственным тестом |
| **C / Shift+C — номера комнат** | у нас номер рисуется палочками именно потому, что текст тянет 2 КБ знакогенератора в W2 (`roomtest.c`). Ждёт своего шрифта |
**Не брать:** `Shift+I` (переворот экрана), `Shift+B` (blind mode) —
развлекательные, к отладке порта отношения не имеют. `/+` (время) — нужен
таймер уровня, которого у нас нет (Фаза 6).
**Отдельно, дорого, но очень ценно — `F6`/`F9` (quicksave/quickload точного
состояния).** Это сериализация `Char` + `room_modif` всех комнат + trob'ов +
состояния стражей. Даёт то, чего нам сейчас сильно не хватает:
воспроизводимый регресс в MAME («вот кадр, где баг») вместо ручного подхода
к позиции. Кандидат сразу после того, как заработают уровни.
---
## 5. Риски и что проверить артефактом до кодинга
1. **Размер кода.** Замер сборки 2026-08-01: `_CODE` 25 119 Б,
куча ~2.4 КБ, банк 2 (`pop_bg`) 13 792 / 16 384, банк 3 (`pop_map`)
6 331, банк 1 (`guards`) 1 896, банк 4 (`pop_gdraw`) 2 236. Чомперы,
зеркало, скелет и второй тайлсет пойдут в банк 2 — там осталось 2.6 КБ.
**Прежде чем начинать §3, посчитать, куда лягут новые тайлы**, иначе
повторится история «банк 2 упёрся в потолок» (коммит 2f3e854). Свободные
номера банков есть (5+), гранулярность — файл.
2. **Спецсобытия уровней** (`seg002.c`: `level3_set_chkp`, `sword_disappears`,
`Jaffar_exit`, зеркало, мышь) — их НЕ надо портировать заранее. Для
уровней 2 и 3 нужен только чекпойнт уровня 3. Остальное — по мере
подхода к уровню.
3. **Чомперы** (уровень 3 и почти все дальше) — отдельная механика
(`animate_chomper` + коллизия + смерть); шаблон работы тот же, что у
пик/ворот, см. `gates_spikes_plan.md`.
4. **`tbl_guard_type = -1`** на уровнях 14/15: без проверки страж
«появится» из неинициализированных данных.
5. **Уровень 0 (демо)** существует в данных, но в скоуп не входит.
-143
View File
@@ -1,143 +0,0 @@
# Loose floors (проваливающиеся полы) — план порта
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose). ПЛАН, ещё не
реализовано. Тайл в комнате 1: `[2,6] = 0x0B = tiles_11_loose`.
## 1. Хранение состояния
- **Тип тайла**: `curr_room_tiles[tilepos] & 0x1F == 11` (tiles_11_loose).
Бит `0x20` = «solid» loose (авто-падающий вариант, ур.13 — от шага НЕ
падает). После падения тайл → `0` (tiles_0_empty).
- **Модификатор** `curr_room_modif[tilepos]` = состояние анимации:
- `0` — покой (обычный loose-пол);
- `0x80..0x83`**трясётся** (бит7); за ~4 кадра затухает обратно в 0;
- `1..11`**обратный отсчёт до падения** (на нём что-то стоит);
достигает `loose_floor_delay = 11` → падает.
## 2. Анимация тряски (shake) — когда включается
- **Триггер = do_knock** (seg007:0FE0): на ЖЁСТКОМ приземлении в кадрах
посадки играет `SEQ_KNOCK_DOWN` → взводит `knock``check_knock()`
`do_knock(room, curr_row (knock>0))`.
- `do_knock(room, row)`: по всем колонкам ряда — если тайл loose →
`loose_make_shake()`.
- `loose_make_shake()` (seg007:0FB4): если `modif==0` (и не ур.13) →
`modif = 0x80`, `add_trob(type 1)`.
- **Отсюда кейс пользователя**: Kid падает/приземляется на `[2,4]`
do_knock трясёт ВСЕ loose-тайлы ряда 2 → `[2,6]` трясётся. (Через
knock-смещение ряда может задеть и соседний ряд.)
- `animate_loose` (кадрово): `++modif`; при бите7 трясёт до `>=0x84`
сброс в 0, `trob.type=-1`. `loose_shake()` играет звук
(sound 20/21/22) по таблице `loose_sound[]`.
## 3. Анимация падения (fall) — когда включается
- **Триггер = make_loose_fall(1)** (seg007:0EF6), вызывается когда:
- Kid СТОИТ на loose-тайле — `check_press()` (seg006): кадр с
FRAME_NEEDS_FLOOR над loose → make_loose_fall(1);
- зацеп/подтягивание на loose (`check_grab`, `check_jump_up`);
- пробой сверху: кадр 79 (jumphang) над loose → make_loose_fall(1);
- авто-падающие (ур.13) — `make_loose_fall(-(prandom&0x0F))`.
- `make_loose_fall(modifier)`: если НЕ solid (`tiles & 0x20 == 0`) и
`(sbyte)modif <= 0``modif = modifier`, `add_trob(type 0)`.
- `animate_loose`: `++modif`; когда `modif >= 11` (loose_floor_delay) →
`remove_loose()` (тайл → empty) + `add_mob()` (спавн падающего куска).
## 4. Падающий кусок (mob)
- `add_mob()` кладёт `curmob` в `mobs[]` (до 14). `do_mobs()` каждый
кадр: `move_mob()` (гравитация, y растёт) + `check_loose_fall_on_kid()`
(урон Киду/страже, если попал).
- Приземление куска → тайл под ним `curr_room_tiles[...] = tiles_14_debris`
(seg007 move_mob:1053). Т.е. **loose(11) упал → сверху empty(0), снизу
debris(14)**.
## 5. Отрисовка по статусу
- Куски тайла: `loose_fram_left[]={41,69,41,70,70,41,41,41,70,70,70,0}`,
`loose_fram_right[]={42,71,...}`, `loose_fram_bottom[]={43,73,...}`
(env-спрайты, seg008:518/596/608).
- Индекс кадра = `get_loose_frame(modifier)` (seg008): `0` = ровный
(41/42/43); `1..10` = дрожащие варианты (69–74); при бите7/большой
задержке — низкие индексы.
- **До падения**: рисуем loose с `get_loose_frame(modif)` (0 = ровно,
иначе колеблется). **После**: сверху empty, снизу debris(14) — обычная
статическая отрисовка (у нас уже есть tile 0x0E/14 debris в tile_table).
## 6. Что нужно в нашем движке (сейчас НЕТ)
Наш `pop_bg` рисует комнату СТАТИЧЕСКИ один раз. Loose-полы требуют
**динамического тайлового слоя**:
1. **Массив модификаторов** `room_modif[30]` (у нас есть `bg[30]` — можно
переиспользовать/рядом) — состояние каждого тайла.
2. **Очередь trob** (список анимируемых тайлов) + `animate_loose` пер-кадр
→ перерисовка ТОЛЬКО изменившихся тайлов (как heal-прямоугольник Kid).
3. **make_loose_fall / do_knock / loose_make_shake** — триггеры (из
физики Kid: приземление→knock, стойка на loose→fall).
4. **mob-система** (падающий кусок): минимум 1–2 mob'а, гравитация,
приземление → debris. Урон Киду (`check_loose_fall_on_kid`) — можно
Фазой 2.
5. **Перерисовка тайла**: `draw_tile(row,col)` у нас уже умеет loose
(`code==11`, `loose_fram_*` в env) — нужно вызывать его выборочно с
текущим модификатором (сейчас draw_tile берёт статический bg).
**Порядок реализации (предложение):**
- L1: room_modif[] + выборочная перерисовка тайла по модификатору
(draw_loose с get_loose_frame) — статика→динамика одного тайла.
- L2: trob-очередь + animate_loose (тряска по do_knock на приземлении).
- L3: make_loose_fall (стойка на loose) + отсчёт + remove → empty.
- L4: mob (падающий кусок → debris снизу).
- L5: урон Киду от падающего куска.
## Конкретика из SDLPoP (сверено 2026-07-18, готово к реализации)
Таблицы (seg008.c), индекс = `get_loose_frame(modif)`:
- `loose_fram_left[] = {41,69,41,70,70,41,41,41,70,70,70,0}`
- `loose_fram_right[] = {42,71,42,72,72,42,42,42,72,72,72,0}`
- `loose_fram_bottom[]= {43,73,43,74,74,43,43,43,74,74,74,0}`
- `get_loose_frame(m)`: если `(m&0x80)` (или delay>11) → `m&=0x7F; if(m>10) return 1;``return m;`
- `y_loose_land[] = {2,65,128,191,254}` (mob), `loose_floor_delay = 11`.
Триггеры (call-sites):
- **make_loose_fall(modifier=1)** — из `check_press()` (seg006): когда Kid
СТОИТ на тайле (FRAME_NEEDS_FLOOR, action < hang_climb / turn / bumped) и
`get_tile_at_char()==11`; ИЛИ `frame==79` (прыжок вверх) и
`get_tile_above_char()==11` (пробой сверху). `tile_is_floor(11)==1`
Kid стоит на loose (start_fall НЕ зовётся). Тело:
`if(!(tile&0x20) && (sbyte)modif<=0){ modif=modifier; add_trob(type0); }`
- **do_knock(row)** — на ЖЁСТКОМ приземлении (SEQ_KNOCK_DOWN→check_knock,
seg003): по всем колонкам ряда `if(tile==11) loose_make_shake()`
(`if(modif==0){ modif=0x80; add_trob(type1); }`).
- **animate_loose** (каждый кадр, seg007:816): `++modif`; если `&0x80`
(тряска): `if(modif>=0x84){modif=0; trob=-1;}`; иначе (отсчёт):
`if(modif>=11){ remove_loose(tile→0); trob=-1; add_mob(); } else shake`.
## Интеграция в наш движок (roomtest) — подход
Наш движок ПЕКЁТ комнату один раз (двойной буфер: своя ОЗУ-копия на
страницу). Loose требует динамики + общего состояния pop_map↔pop_bg:
1. **Общая МУТАБЕЛЬНАЯ копия комнаты**: roomtest.c держит `uint8_t
room_fg[30]` (копия room1_fg) и передаёт ОДИН указатель и в
`pop_room_draw`, и в `pop_map_set` → мутации loose видны обоим.
(Сейчас g_fg — `const`; сделать неконстантным.)
2. **Состояние**: `uint8_t pop_loose_modif[30]` (0 / 0x80.. / 1..11).
3. **Модель** (pop_map): `check_press()` в `pop_phys_tick` (make_loose_fall
при стоянии на 11), `animate` каждый кадр.
4. **Перерисовка** (pop_bg, на back-странице каждый кадр):
- тряска/отсчёт (код всё ещё 11): `gfx_heal(tile rect)` (вернуть
печёный фон) + нарисовать loose-кадр (left/right/bottom по
get_loose_frame) банком SPRITE;
- падение (11→0): mutate `g_fg[pos]=0` + «запечь пустоту» на ОБЕИХ
страницах (bake-счётчик 2: чёрный bar + draw_tile empty банком NORMAL
на текущей странице 2 кадра подряд) → дальше heal показывает пусто.
5. **L4 mob**: падающий кусок → debris(14) снизу (y_loose_land); пока
отложено — упавший loose = пусто. **L5** урон — позже.
Риск: перерисовка динамического тайла в двойном буфере (per-page heal +
bake) — единственное тонкое место; остальное — прямой порт логики выше.
## Связанные
Триггеры завязаны на физику Kid ([[pop_hang_state]] check_press/check_grab,
приземление land/SEQ_KNOCK_DOWN). Отрисовка — [[pop_fore_layer]] /
[[pop_background_strategy]] (draw_tile уже знает loose_fram_*).
+148
View File
@@ -0,0 +1,148 @@
# Генераторы псевдослучайных чисел: запасные варианты
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально
можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра —
**сейчас менять ничего не нужно**.
## Что стоит сейчас
`pop_geom.c`, ветка `POP_PRANDOM_EXACT=1` (по умолчанию) — LCG оригинала
`s = s*214013 + 2531011`, шаг написан на Z80-ассемблере (единственное такое
место в порте). Схема Горнера по разреженной записи константы:
```
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
```
17 удвоений, три сложения, одно вычитание; величина `3*s`, нужная в конце,
попадается по дороге на втором шаге. Тело — **≈1 020 тактов** по статическому
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
с эталоном.
Вторая ветка, `POP_PRANDOM_EXACT=0` — xorshift16 + шаг Вейля на C.
Совместимость теряется.
Замер в MAME, комната 3, 175 кадров (медиана кадра):
| вариант | кадр | prandom → torch_draw |
|---|---|---|
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
| C, xorshift16 + Вейль | 397 986 | 7 927 |
| **asm, бит-в-бит (сейчас)** | **400 800** | **9 331** |
## Вариант A — комбинированный LFSR + LCG, ~148 тактов
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
```z80
prng16:
seed1=$+1
ld hl, 9999
ld b, h
ld c, l
add hl, hl
add hl, hl
inc l
add hl, bc
ld (seed1), hl
seed2=$+1
ld hl, 987
add hl, hl
sbc a, a
and 101101b
xor l
ld l, a
ld (seed2), hl
add hl, bc
ret
```
Устройство: `seed1` — LCG `x = 5x + 1` (по модулю 2^16; `inc l` вместо
`inc hl` — экономия байта, на период не влияет). `seed2` — 16-битный
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
`0x2D` (примитивный многочлен `x^16 + x^5 + x^3 + x^2 + 1`). На выходе
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
**Что мешает взять как есть:** сиды зашиты в код (SMC), а нам нужны ДВЕ
независимые последовательности — раскладка кладки и анимация тайлов.
Пришлось бы передавать состояние через указатель, как сейчас у `pop_prandom`
(это +20…40 тактов, не принципиально).
## Вариант B — xorshift(7,9,8), ~86 тактов
Самый быстрый, период 65535.
```z80
xrnd:
ld hl, 1 ; seed must not be 0
ld a, h
rra
ld a, l
rra
xor h
ld h, a
ld a, l
rra
ld a, h
rra
xor l
ld l, a
xor h
ld h, a
ld (xrnd+1), hl
ret
```
**Две оговорки.** Ноль — неподвижная точка, а сид раскладки кладки у нас
считается как `номер комнаты + смещение ряда + колонка` и вполне может
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
что в варианте A.
## Чего НЕ брать: RND из Apple II
Оригинальный `Prince-of-Persia-Apple-II`:
```
RNDseed := (5 * RNDseed + 23) mod 256
```
```asm
RND
lda RNDseed
asl
asl
clc
adc RNDseed
clc
adc #23
sta RNDseed
rts
```
Полный период 256 (`a ≡ 1 mod 4`, `c` нечётное), и для своего движка он
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
`prandom(1)` (ОДИН бит) и `prandom(4)`, то есть вместо шума получилась бы
аккуратная шахматка.
## Сколько реально можно выиграть
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
весь расход: остаются обёртка `pop_prandom`, приведение к диапазону
`pop_rnd_fit` и ABI вызова. Верхняя граница выигрыша видна из замера выше:
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
**2 814 тактов за кадр (0.65 %)** при двух вызовах за кадр, и это ПОТОЛОК —
любой из вариантов A/B ниже него не опустится.
Порядок действий, если понадобится:
1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы
на вызов приходился один `call`, а не три. Это ничего не ломает и не
трогает совместимость с эталоном.
2. И только если этого мало — менять генератор, начиная с варианта A
(качество последовательности у него не хуже LCG, в отличие от B).
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают
`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и
цена вопроса вырастет во столько же раз.
+86
View File
@@ -0,0 +1,86 @@
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
> **Статус: ЖИВОЙ ПЛАН, сделан частично (сверено 2026-08-01).**
> - **S1 — сделан:** `kid_room` заведён отдельно от `cur_room`,
> `update_kid_render_dx()` (`roomtest.c`) даёт рендер-смещение ∓140, а
> `pop_kid_set_render_dx` применяет его в отрисовке. Фактически это пока
> каркас: `enter_room` держит `kid_room == cur_room`, так что смещение
> всегда 0.
> - **S2/S3/S4 — не сделаны и не срочны.** Исходный повод (баг #4,
> пинг-понг у шва) закрыт иначе — поправкой odd-pixel в
> `char_x_forward_edge` + `pop_leave_timer` (разбор корня —
> `../roomtest/bug_closed.md`, BUG-SEAM-PINGPONG).
>
> **Зачем документ остаётся.** Полная straddle-модель понадобится для:
> (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4),
> (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у
> шва (S4). Брать из `../roomtest/TASKS.md`, когда дойдёт очередь.
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
## Факты из SDLPoP (подтверждено чтением исходника)
- `Char.room` (реальная комната персонажа) ≠ `drawn_room` (отрисованная) —
штатное состояние.
- **Коллизия через ±140:** `xpos_in_drawn_room()` (seg004:0405) сдвигает
xpos на `±TILE_SIZEX*SCREEN_TILECOUNTX = ±140`, когда `curr_room` колонки
(`curr_row_coll_room[col]`) ≠ `drawn_room` (room_L/room_BL → 140,
room_R/room_BR → +140). Т.е. коллизия строится по РЕАЛЬНЫМ тайлам соседей.
- **Смена экрана:** `check_the_end()` (seg000:0FBD): `if (next_room!=0 &&
next_room!=drawn_room) { drawn_room=next_room; load_room_links; redraw }`.
`next_room` ставится в `exit_room()` (= `Char.room` ПОСЛЕ успешного
`leave_room`). Значит drawn_room следует за Char.room, но Char.room меняется
ТОЛЬКО при реальном пересечении шва (leave_room, seg002:0504) на «легальном»
кадре/действии (не turn/climb/standup).
- **Отрисовка левого соседа:** только `load_leftroom()` (col9 левого соседа в
левую кромку); правый сосед НЕ рисуется (изометрия). Окклюзия ворот на шве —
только левая (seg008:696).
- **Ceiling-полоса:** `draw_room` рисует доп. ряд из `room_A` (row2, draw_main_y
=-1). (Уже реализовано, баг #3.)
## Текущее состояние нашего движка (до #4)
`cur_room` (=drawn_room) ВСЕГДА == комната Kid. Шов подделан: Kid остаётся в
drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol` (коллизия ±1
кол) / `lcol_bg` (openness ворот). Уход из комнаты — `pop_leave_dir`/`enter_room`
МГНОВЕННО при пересечении порога `char_x`. Отсюда #4: экран переключается
раньше, чем в оригинале (Kid должен «отступить» за кромку, оставив старую
комнату).
## План (инкременты, каждый проверяется в MAME)
### S1. Данные + рендер-смещение Kid
- Ввести `kid_room` (реальная комната Kid) отдельно от `cur_room`(=drawn_room).
- `kid_x_offset()` = разница комнат: kid_room == left(drawn) → лог. x Kid 140
(рисуется за левой кромкой); right → +140; равны → 0. (порт
xpos_in_drawn_room).
- `kid_draw`/heal/fore используют смещение (Kid рисуется частично за кромкой).
- Проверка: Kid у шва рисуется со сдвигом, экран не дёргается.
### S2. Коллизия по kid_room
- Коллизионный контекст (`g_fg`/edges/`g_room`/modif в pop_map) следует за
`kid_room`, а не за drawn_room. Когда kid_room≠drawn_room — грузим
соседа как коллизионную комнату (curr_col 0..9 в кадре kid_room).
- Отрисовка (room_fg и т.п.) остаётся по drawn_room.
- Порт `curr_row_coll_room[]`/`xpos_in_drawn_room` можно упростить: держим
ОДИН коллизионный room (kid_room) + существующие снапшоты кромок для ±1 кол.
### S3. Отложенная смена drawn_room
- Уход (`check_leave`/`check_leave_below`): ставит `kid_room=сосед`,
репроецирует Kid (x∓140, col∓10) — но drawn_room НЕ меняет сразу.
- `check_the_end`-эквивалент в главном цикле: `if (kid_room != drawn_room &&
<условие коммита>) enter_room(kid_room)`. Условие коммита — по SDLPoP:
как только Char.room сменилась легальным leave (не bumped/turn). Для
«bumped назад за кромку» drawn_room остаётся (симптом #4).
- Проверка сценариев #4/#5 в MAME.
### S4. Полировка
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
## Связанные баги — все ЗАКРЫТЫ (`../roomtest/bug_closed.md`)
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
колонны) — починены без полной straddle-модели. То есть S4 «полировка
окклюзии» осталась актуальной только для окклюзии У ШВА при straddle.
Memory: `pop_seam_room_model`.
Binary file not shown.
@@ -27,6 +27,8 @@ static const char *const pop_env_atl[POP_ENV_PAGES] = {
};
#define POP_WALL_ATL "pop_wall.atl"
#define POP_FORE_ATL "pop_fore.atl"
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */
#define POP_PAL_POT 0x40
#define POP_BG_PAL "pop_bg.pal"
#endif
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -36,4 +36,6 @@ static const char *const kid_atl[KID_PAGES] = {
"kid27.atl",
};
#define KID_PAL_FILE "kid.pal"
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
#define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
#endif
Binary file not shown.
Binary file not shown.
+80
View File
@@ -0,0 +1,80 @@
# roomtest — правила подпроекта
Живой прототип порта PoP: комната 1 уровня 1 (композиция тайлов в рантайме)
+ Kid (анимация seqtbl, управление, коллизия, падение, зацеп, fore-окклюзия,
проваливающиеся полы). Действуют правила корневого `CLAUDE.md` и
`applications/PoP/CLAUDE.md`.
**Главное правило (из `../CLAUDE.md`): `../SDLPoP/src/` — источник истины.**
Перед реализацией ЛЮБОЙ функции и по ЛЮБОМУ вопросу «как в оригинале» —
СНАЧАЛА прочитай соответствующий код SDLPoP и портируй по нему; не пиши
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
по умолчанию баг у нас.
**Что в работе сейчас — `TASKS.md`** (доска задач: приоритеты, критерии
готовности). Баги — `bug_list.md` (только ОТКРЫТЫЕ) и `bug_closed.md`
(закрытые + разбор корней: перед заведением нового бага грепни там по
симптому). План следующих уровней — `../docs/levels_plan.md`.
## Сборка и запуск
```
make # собрать roomtest.exe (упаковав ассеты через toolchain/)
make run # exe + EXTRA_DATA на дискету + запуск MAME (см. корневой док)
make -C tests-host # модульные тесты движка под ucsim_z80 (секунды, без MAME)
```
Логику, которую можно проверить без железа, покрывать в `tests-host/`
(обвязка — `testkit/`, там же почему прогон именно под z80). MAME остаётся
для отрисовки, банков, таймингов и клавиатуры.
`MEMORY=small`, `--gfx 256`. Ассеты (`pop_env0..4.atl`, `pop_wall.atl`,
`pop_fore.atl`, `kid0..27.atl`, `kid.pal`) генерируются python-скриптами
`../toolchain/` — Makefile дёргает их сам при изменении. Данные комнаты —
`room1_data.h` (fg=foretable code, bg=backtable modifier); данные анимации
Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
## Проверка в MAME
Только через `toolchain/mame_interactive.py` или MCP-мост `mame-z80`
(см. memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
Эталон комнаты — `../toolchain/1.1-2.png`. Пересобрал HDD-образ → MAME
ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
**ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I**
бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень,
**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру
(`pop_level_load_num`), на диске лежат все 15.
## Модули
| Файл | Роль (порт SDLPoP) |
|------|--------------------|
| `roomtest.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), tick→draw порядок слоёв. |
| `pop_bg.c/.h` | Отрисовка фона композицией тайлов (`seg008` draw_tile/wall_pattern), fore-окклюзия поверх Kid (`seg003` redraw_at_char), loose-полы (shake/bake/mob, `seg007/008`). |
| `pop_kid.c/.h` | Анимация/движение Kid: интерпретатор seqtbl `play_seq` + frame_table (`seg006`). |
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
## Порядок слоёв в кадре (важно для окклюзии)
`roomtest.c` каждый тик рисует в СКРЫТУЮ страницу: `kid_heal` (стереть
прошлый кадр) → `kid_tick``pop_phys_tick``pop_loose_tick` (loose СЛОЙ
ФОНА — ДО Kid, чтобы Kid был поверх плиты) → `kid_draw``pop_fore_over_kid`
(передние грани тайлов поверх Kid) → `pop_room_clip_borders` (чистка бортов,
гейт по флагу — только в кадрах падения) → флип на vsync.
**Дабл-буфер:** у каждой страницы своя видео-ОЗУ и теневая ОЗУ-копия; heal
берёт чистый фон из копии ТОЙ страницы, в которую рисуем. Любой динамический
элемент (Kid, loose-плита, падающий кусок) обязан чистить свой прошлый кадр
на КАЖДОЙ из двух страниц — иначе остаток виден через кадр как **мерцание**.
Это типовой источник багов «остаётся кусочек» (см. memory
`pop_fall_debug_baseline`).
## Ключевые memory
`pop_kid_plan`, `pop_hang_state`, `pop_fore_layer`, `pop_fall_debug_baseline`,
`accel_vertical_copy`, `kbd_raw_fifo_drain`, `pop_check_sdlpop_first`.
+73 -8
View File
@@ -5,30 +5,95 @@ PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := roomtest
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
MEMORY ?= small
EXTRA_FLAGS ?= --gfx 256
EXTRA_SRCS := pop_bg.c pop_kid.c pop_ctrl.c pop_map.c
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
# Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_gdraw
# в одной странице — вызовы между ними прямые, если функция не помечена
# __banked). Так снято главное ограничение резидента: из банка его было
# не достать ни прямо, ни транзитивно, а банк→банк работает (трамплин
# сохраняет страницу окна на СТЕКЕ, проверено вложенной цепочкой
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
# sdcc_banked_call_rules.
# Банк 5 = pop_ctrl.c (MEM-BANK5). CODE и DATA делят одно 32-КБ
# пространство W1+W2, поэтому килобайт кода, уехавший в банк, — это
# килобайт, доступный данным. Кандидат выбран не по размеру, а по частоте
# вызова: диспетчер управления дёргается один раз за кадр на персонажа и
# горячих банк→банк переходов не создаёт (в отличие от pop_level, чей
# pop_level_tile зовётся из банка 2 на КАЖДЫЙ тайл). Шина control_*
# переехала в pop_state.c — см. там.
# --max-allocs-per-node: во сколько вариантов размещения регистров SDCC
# упирается на узел. У sprinter-cc дефолт 100000 (агрессивно, как fast-сборки
# библиотек) — на roomtest это МИНУТЫ на банк, потому что модули крупные.
# Дефолт SDCC — 3000, и для разработки его достаточно: разница в размере
# единицы процента, а сборка кратно быстрее. Замер (2026-08-05) — в шапке
# TASKS.md, запись BUILD-FAST.
#
# make — быстрая сборка (ALLOCS=3000)
# make ALLOCS=100000 — как раньше: минимальный код, для замеров размера
# и для «релизного» образа
#
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
ALLOCS ?= 3000
EXTRA_FLAGS ?= --gfx 256 --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
# MAME = время фазы в растровых строках.
PROF ?= 1
PROF_FLAGS := -DPROF_BORDER=$(PROF)
EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_level.c pop_redraw.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
# ВСЕ уровни игры (res2001..res2015, по 2305 Б = 34 КБ на всех): переход
# уровень→уровень и чит Shift+L требуют, чтобы файл следующего лежал на
# диске. res2000 — демо-уровень, в скоуп не входит.
LVL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
LVL_DATA := $(foreach n,$(LVL_NUMS),$(LVL_DIR)/res20$(n).bin)
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \
$(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \
$(BG_DIR)/pop_pot.atl
# ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23)
# не рисуются (kid_draw пропускает незагруженную страницу). Палитра —
# ЕДИНАЯ игровая (env+wall+kid).
KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl)
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl
# Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его
# делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже.
KID_BIN := $(KID_DIR)/kid_data.bin
GUARD_DIR := $(CURDIR)/../poc/res/guard
GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \
$(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl
EXTRA_DATA := $(BG_DATA) $(KID_DATA)
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(LVL_DATA)
include $(PROJ_ROOT)/app.mk
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
# чтобы корень диска не зарастал десятками .atl.
# BG\ — фон (env/wall/fore/pot)
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
# GUARD\ — стражи (появятся здесь же)
# LEVELS\ — уровни
hdd: $(EXAMPLE).exe
$(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \
$(EXAMPLE).exe \
$(foreach f,$(BG_DATA),BG:$(f)) \
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
$(foreach f,$(LVL_DATA),LEVELS:$(f))
TC := $(PROJ_ROOT)/applications/PoP/toolchain
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
cd $(TC) && python3 pop_pack_bg.py
$(GUARD_DATA): $(TC)/pop_pack_guard.py
cd $(TC) && python3 pop_pack_guard.py
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
cd $(TC) && python3 pop_pack_kid.py
kid_data.h: $(TC)/pop_extract_kid_data.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
cd $(TC) && python3 pop_extract_kid_data.py
$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h kid_data.h room1_data.h $(BG_DATA) $(KID_DATA)
$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_draw.c _pop_draw.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
+52
View File
@@ -0,0 +1,52 @@
# roomtest
Живой прототип порта Prince of Persia: комната 1 уровня 1 отрисовывается
реальной композицией слоёв-тайлов, по ней бегает и прыгает Kid под
управлением с клавиатуры. Служит стендом для переноса механик движка
(анимация, коллизия, падение, зацеп, окклюзия, проваливающиеся полы) до их
выделения в переиспользуемый код.
Правила разработки и порядок слоёв рендера — [`CLAUDE.md`](CLAUDE.md).
## Быстрый старт
```
make # собрать roomtest.exe (ассеты упакуются автоматически)
make run # собрать, упаковать на дискету и запустить в MAME
```
Клавиши в запущенном тесте: ←/→ бег, ↓ присед, ↑+→ прыжок, Shift шаг,
ESC выход.
**Отладка рисования:**
- **SPACE** — тумблер дабл-буфера (в однобуфере баги рисования видны без
мерцания-через-кадр).
- **1** — заморозить кадр (`frozen`: tick/draw/flip пропускаются, картинка
стоит), **2** — снять заморозку и продолжить. Приём для разбора
транзиентных артефактов (тряска/падение loose, поза Kid): поймать
проблемный кадр на `1`, спокойно рассмотреть/снять пиксели — в т.ч.
удалённо из MAME MCP-моста, пока анимация стоит.
Эталон вида комнаты — `../toolchain/1.1-2.png`.
## Модули
- `roomtest.c` — главный цикл (дабл-буфер, порядок tick/draw).
- `pop_bg.c` — фон комнаты композицией тайлов + fore-окклюзия + loose-полы.
- `pop_kid.c` — анимация/движение Kid (интерпретатор seqtbl).
- `pop_ctrl.c` — управление с клавиатуры (диспетчер control).
- `pop_map.c` — коллизия с картой, физика падения, loose-полы.
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
(оба генерируются скриптами `../toolchain/`).
## Статус (2026-08-01)
Играется весь уровень 1: комнаты и переходы между ними, Kid (анимация,
управление, коллизия/падение, зацеп/подтягивание/спуск, окклюзия), кнопки и
ворота, пики, проваливающиеся полы, дверь уровня, меч и бой, стражи с ИИ,
HP и зелья.
Не сделано: выход в следующий уровень (дверь открывается, но войти в неё
нельзя), звук, таймер/HUD времени, сохранения. **Что берём в работу
сейчас — [`TASKS.md`](TASKS.md)**; план следующих уровней —
[`../docs/levels_plan.md`](../docs/levels_plan.md).
+822
View File
@@ -0,0 +1,822 @@
# roomtest — доска текущих задач (обновлено 2026-08-04)
Не список багов (открытые — [`bug_list.md`](bug_list.md), закрытые с разбором
корней — [`bug_closed.md`](bug_closed.md)) и не план фаз
(`../docs/PORT_PLAN.md`, `../docs/layout_plan_v2.md`, `../docs/levels_plan.md`),
а то, **что берём в работу сейчас и в каком порядке**. Каждая запись: что
сделать, почему именно сейчас, чем подтверждать результат.
**Открытых багов нет** ([`bug_list.md`](bug_list.md)): остались незакрытые
оптимизации [T-1](bug_list.md#t-1), [T-2](bug_list.md#t-2) и незаконченная
[таблица обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1).
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
гипотезой (memory `defer_unexplained_quirks`).
---
## ТЕКУЩАЯ ЦЕЛЬ: уровни 1–3 (подземелье) отлажены целиком
Решение 2026-08-04: **palace (уровни 4+) откладываем**, доводим до
играбельности три dungeon-уровня. Основание — они не требуют ни одного
нового ассета фона: инвентарь тайлов, снятый с `res200N.bin`, показывает,
что новое появляется только так —
```
ур. 1 empty, floor, spike, pillar, gate, closer, potion, loose, debris,
opener, level_door L/R, torch, wall, skeleton, sword ← всё есть
ур. 2 bigpillar_bottom(8), bigpillar_top(9), doortop(12) ← есть (2026-08-04)
ур. 3 chomper(18) ← НЕТ механики
ур. 4 lattice_pillar(25)…lattice_right(29) + тайлсет palace ← отложено
```
**Порядок (решение пользователя 2026-08-04): СНАЧАЛА приёмка уровня 2,
потом уровень 3.** Причина техническая, а не вкусовая: чомперы лягут в
тот же банк 2, где живёт отрисовка, и чинить баги фона поверх свежей
механики дороже, чем до неё.
| # | Задача | Что | Блокирует |
|---|--------|-----|-----------|
| 1 | [L2-PASS](#l2-pass) | приёмка уровня 2 + баги отрисовки | ← **сейчас** |
| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 |
| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 |
| 4 | [L3-CHKP](#l3-chkp) | чекпойнт ур. 3 | корректный респавн ур. 3 |
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 |
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
| ✔ | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | **СДЕЛАНО 2026-08-05** |
| ✔ | [MEM-BANK5](#mem-bank5) | `pop_ctrl.c` → банк 5 | **СДЕЛАНО 2026-08-05**: куча 180 Б → 2298 Б |
---
## Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3
Сюда попадает то, что уже работает в проверочном прогоне, но должно быть
подтверждено на сквозных прогонах уровней — потому что задевает механику
шире, чем собственный сценарий.
- **Зацеп ПРЯМО В ПРЫЖКЕ** (`POP_ENABLE_JUMP_GRAB`, `pop_tune.h`, сделан
2026-08-06, предварительно проверен пользователем). Почему нужен именно
финальный прогон: точки вызова стоят не только в `check_action`, но и в
ОБЕИХ ветках `check_bumped` — то есть код вклинивается перед обычным
ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о
стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На
уровнях 1–3 это надо специально потрогать в паре мест каждого уровня.
Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — `enable_jump_grab`),
так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при
включённых enhancements.
## P0 — делаем сейчас
### <a id="l2-pass"></a>L2-PASS. Приёмка уровня 2 — **SMOKE ПРОЙДЕН 2026-08-05**
> **Прогон 2026-08-05 (пользователь): уровень 2 пройден.** Полная приёмка по
> всем комнатам — позже; из smoke пришли [BUG-LOOSE-3](bug_list.md#bug-loose-3)
> и [BUG-GUARD-DEAF-1](bug_list.md#bug-guard-deaf-1). Следом прогнан smoke
> уровня 3 — его наблюдения в [разделе «Уровень 3»](bug_list.md#уровень-3).
Баги отрисовки уровня 2 пользователь подаёт списком отдельно — они идут в
[`bug_list.md`](bug_list.md), раздел «Уровень 2». Ниже — **карта
содержимого уровня, снятая прямо с `res2002.bin`**, чтобы приёмка шла по
списку, а не «на глаз»: если механика в таблице есть, а в игре не
сработала — это баг, а не «так задумано».
**Стражи — 5, в комнатах 4, 7, 11, 15, 24** (skill 1/2/1/1/3, цвета
1/3/1/1/6 — цвет мы пока игнорируем, атлас один). Заметить: страж
комнаты 24 со skill 3 — первый по-настоящему опасный.
**Кнопки и что они открывают** (декодировано из LINKLOC/LINKMAP):
| Кнопка | Тип | Цель |
|--------|-----|------|
| к.9 @ряд1,кол1 | RAISE | **дверь уровня** к.23 @1,3 — то есть выход |
| к.11 @1,1 | RAISE | ворота к.18 @0,9 |
| к.18 @0,7 | RAISE | ворота к.7 @0,9 **и** к.18 @0,9 (две сразу) |
| к.18 @0,2 | DROP | закрывает ворота к.7 @0,9 |
| к.13 @1,4 | DROP | закрывает ворота к.13 @1,5 |
**Ловушки и предметы по комнатам:**
```
к. 3 loose@2,2 зелье@2,5 (здоровье)
к. 4 loose@1,7 loose@1,8 + СТРАЖ
к. 5 дверь уровня @1,2-3 — ВХОД (захлопывается за спиной)
к. 6 пики@1,3 loose@1,5 зелье@2,7 (здоровье)
к. 7 ворота@0,9 пики@2,7 + СТРАЖ
к. 8 зелье@2,2 (здоровье)
к. 9 кнопка RAISE@1,1 loose@2,6
к.10 пики@2,2
к.11 кнопка RAISE@1,1 + СТРАЖ
к.12 зелье@2,6 (здоровье) loose@2,8
к.13 зелье@1,3 (ВРЕДНОЕ, −1 HP) кнопка DROP@1,4 ворота@1,5 зелье@1,8
к.15 + СТРАЖ
к.18 кнопка DROP@0,2 loose@0,4 кнопка RAISE@0,7 ворота@0,9 зелье@2,6
к.19 пики@1,4
к.20 ЗЕЛЬЕ@1,2 — БОЛЬШАЯ СКЛЯНКА (+1 к потолку HP) пики@2,6 пики@2,7
к.23 дверь уровня @1,3-4 — ВЫХОД
к.24 + СТРАЖ (skill 3)
```
**Отдельно проверить то, что сделано именно сегодня и на уровне 1 не
проверялось:**
- **большая склянка** (к.20) — потолок HP становится 4, индикатор рисует
четыре деления, и это HP **переносится на уровень 3**;
- **меч уже в руках** с самого старта (`have_sword = level >= 2`) — на
уровне 1 его надо было подбирать;
- **выход через дверь к.23** вживую (кнопка в к.9) — путь тот же, что
закрыт на уровне 1 (L1-EXIT), но на этом уровне не прогонялся;
- **смерть/респавн** возвращают на уровень 2, а не на 1.
### <a id="rooms-graph"></a>Связность комнат уровней 1–3 (снято 2026-08-05)
Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет
соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись
13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый,
в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии.
| уровень | старт | недостижимы | признак |
|---------|-------|-------------|---------|
| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет |
| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы |
| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** |
```
ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7
23 R→22, у 22 L=4 | 24 R→7, у 7 L=2
| 24 U→16, у 16 D=0
на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty
```
То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в
брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот
же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю
колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов
там низкий, в игре их не видно.
**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная
связь — вверх, откуда Кид падает):
```
к.10 U→4 пики(2,2) + пол — падение из комнаты 4
к.14 U→21 шахта 2 тайла шириной, дно = обломки
к.17 U→15 то же
```
Это не баги данных: в 14/17 попадают только падением насмерть, а из 10
(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать
«застрял» багом.
### <a id="l3-chomp"></a>L3-CHOMP. Чомперы — механика уровня 3
**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти
на каждом уровне, так что это вложение не только в ур. 3.
**Что портировать:** `animate_chomper` (seg007) — состояние в `room_modif`,
как у пик/ворот, значит шаблон уже отработан (`gates_spikes_plan.md`);
коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка
кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`).
**Риск:** банк 2 (`pop_bg`) занят на **86.5 %, свободно 2213 Б** — считать
место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся
в потолок» (коммит 2f3e854). Свободные номера банков есть (5+).
### <a id="l3-skel"></a>L3-SKEL. Скелет — единственный противник уровня 3
**Важно:** в данных уровня 3 **стражей нет вообще** (`guards_tile` пуст во
всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных»,
а **спецсобытие** `check_skel` (seg002:1044):
> в комнате 1, когда `Kid.curr_col` == 2 или 3 и дверь уровня открыта,
> тайл `tiles_21_skeleton` (комната 1, тайлпос 15) стирается в пол, а на
> его месте поднимается персонаж: `charid_4_skeleton`, меч сразу вынут,
> `seq_88_skel_wake_up`, skill 2, HP 3.
Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают.
**Что нужно:**
- `charid_4_skeleton` в `enter_guard`/`pop_guard_enter` (seg002:196): при
`tbl_guard_type[level] == 2` персонаж поднимается **с вынутым мечом** и
последовательностью `seq_63_guard_active_after_fall`, а не
`seq_77_guard_stand_inactive`;
- скелет **бессмертен** — чит `K` его уже не берёт (`pop_guard_kill`
проверяет `CHARID_4_SKELETON`), но и боёвка должна возвращать его к
жизни (seg002:252);
- **новый атлас**: `../SDLPoP/data/SKEL/` (29 файлов) — `pop_pack_guard.py`
сейчас прибит к `GUARD/`, нужен параметр набора (`tbl_guard_dat` =
GUARD/FAT/SKEL/VIZIER/SHADOW).
### <a id="l3-chkp"></a>L3-CHKP. Чекпойнт уровня 3
`level3_set_chkp` (seg002:519): вход в комнату 7 ставит `checkpoint = 1` и
`hitp_beg_lev = hitp_max`. `do_startpos` (seg003:141) при `checkpoint`
подменяет старт: комната 2, тайлпос 6, направление влево, и убирает
loose-плиту (комната 7, колонка 4, ряд 0). Механика `hitp_beg_lev` уже
есть (сделана в L2), остаётся сам флаг и подмена старта.
### <a id="tune-1"></a>TUNE-1. Параметры движка — в конфиг, а не в код
**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном
заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки
(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`).
Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а
не константой по месту.
**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без
пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под
моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа,
комментарии `;`, неизвестные ключи игнорировать), файл необязателен —
нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`,
`[debug]`, `[enhancements]`.
**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) +
`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его
(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и
опций — часть задачи: у него уже разложены по группам стартовые
HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало,
чекпойнт), тайминги ворот и пик, скорости, а отдельной группой —
`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже
портировали). Брать всё подряд не надо: переносим по мере того, как
константа реально понадобилась в игре.
**Оговорка по памяти.** Парсер и таблица параметров — холодный код,
исполняется один раз при старте: кандидат в банк, а не в резидент W1.
### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН**
> **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как
> установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой
> замер потока байт это не подтвердил: клавиатура MAME-Sprinter
> (`pc_kbd ms_naturl`) обёртку `E0 F0 12` / `E0 12` не шлёт вовсе — при
> зажатом Shift поток на стрелку ровно `E0 75 E0 75 …`. Значит удвоения
> трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO
> переполняется» отпадает; сам ФИКС (плотный опрос `kbd_raw_poll`) остаётся
> верным и нужным — переполнение вызывает не Shift, а короткая жизнь
> импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия
> — [BUG-KBD-5](bug_closed.md#bug-kbd-5).
>
> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: при
> зажатом Shift PS/2 удваивает трафик («fake shift»), а импульс запроса
> прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO
> SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен
> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) —
> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча.
>
> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home →
> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже:
> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147.
> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в
> простое). Ниже — полный протокол, как к этому пришли.
>
> **ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно
> лучше, но иногда при зажатом Shift стрелка всё-таки пропускается».**
> Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали,
> значит остаточная частота заметно ниже прежних ~15 %. **Задача осознанно
> ОТЛОЖЕНА до финальной полировки всей программы** (решение пользователя);
> сейчас клавиатура пригодна для работы.
>
> **Где именно осталась дыра — чтобы на полировке не начинать с нуля.**
> Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это
> занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при
> допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё
> ещё может потерять байт — ровно «иногда». Порядок действий, если
> вернёмся:
> 1. Вернуть вызовы `kbd_raw_poll()` между блитами занятой фазы (они
> бесплатны; сами по себе не помогали, но вместе с хуком закрывают
> именно этот зазор) и при необходимости внутрь тайловых циклов
> `pop_bg` — тогда слепым остаётся только тело одного блита.
> 2. Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые
> нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий.
> 3. Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code
> Set 3 через BIOS `$EA` (убирает «fake shift» в корне, но в MAME
> непроверяемо — обратный путь к клавиатуре не разведён) и общий
> `m_irq_off_timer` в драйвере MAME.
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
нажатия ← не отрабатываются, пока Shift не отпустишь.
**Рабочая гипотеза (механизм, а не догадка «что-то с клавиатурой»).**
Три известных факта складываются в одну картину:
1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ
клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием
шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было
бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3). То есть
ровно в связке Shift+стрелка трафик удваивается.
2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то,
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
«нажатие не отработало»; потерянный break = залипание (его лечит
`kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш).
3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.**
`mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()`
выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая
запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же
заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию.
**Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт
остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц).
4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит
(`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»);
порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это
сотни микросекунд, на порядки больше 32-тактового импульса.
Отсюда: **обе версии пользователя — про одно и то же.** Логика ввода
(`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с SDLPoP и
выглядит корректной: `safe_step()` ставит `control_forward = CONTROL_IGNORE`,
и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть
повторные тапы ← при зажатом Shift обязаны работать. Не работают они
потому, что до нас не доезжает либо make, либо break стрелки.
**План проверки — по шагам, каждый даёт артефакт:**
1. Счётчики в MAME: брейк на `_kbdraw_overrun` (запись) и на ветке
`tr_kbd_drain` — сколько overrun'ов за 10 с при «Shift зажат, тапаю ←»
против «тапаю ← без Shift». Ожидание по гипотезе: с Shift кратно больше.
2. Замер максимального DI-окна кадра: брейкпоинты на `di`/`ei` в
`_bgi_blit_cols_raw` + `{printf totalcycles; g}` — получить реальную длину
в тактах и в микросекундах.
3. **Спайк «блит без DI».** `docs/new/06-accel.md §6.6`: новая прошивка
допускает работу акселератора при EI (по приходу прерывания он
отключается, по `RETI` включается обратно); старая — нет. Собрать libbgi
с убранным `di` в блит/heal-ядрах, прогнать roomtest в MAME: (а) не
рушится ли картинка, (б) падает ли счётчик overrun из п.1. Если да —
причина подтверждена, и дальше это вопрос «какая прошивка на живом
железе» (по умолчанию оставить DI, режим без DI — опцией libbgi).
4. **Независимо от п.3 — `kbd_raw_poll()`.** Вычерпывание FIFO ОПРОСОМ
(порт `0x19` бит 0 → читать `0x18`, тот же декодер make/break, что в
трамплине) из главного цикла 2–4 раза за кадр между фазами `PROF()`.
Снимает зависимость от «поймали ли мы импульс IRQ» вообще, стоит сотни
тактов, графику не трогает. Реализация: вынести drain-цикл из
`libc/irq/_irq_tramp.c` в общий кусок либо продублировать в
`libc/kbd/kbd_raw_poll.c`; тело обязано идти под `DI` (гонка с ISR за
деструктивное чтение порта 0x18).
5. Побочно сюда же играет **[T-2](bug_list.md#t-2) (idle-skip)**: не
перерисовывать Кида, пока поза/координаты не менялись, — это минус
heal+blit (то есть минус DI-окна) в самых спокойных кадрах, где как раз
и тапают Shift+стрелку.
6. Только если после 3–4 симптом жив — копать логику
`control_shift2`/`CONTROL_IGNORE` против `seg005.c:374..390`.
**Критерий готовности:** при зажатом Shift десять тапов ← дают десять
осторожных шагов (проверка в MAME через `:kbd:ms_naturl:*` напрямую, НЕ
через `press_key` — тот дёргает обе клавиатуры, см. `docs/libc-reference.md`
`<kbd_raw.h>`).
---
### KBD-1: ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась
**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не
спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же
`E0`-префикс и тот же «fake shift», что у стрелок), но игрой игнорируется,
поэтому Кид стоит на месте и рельеф комнаты на результат не влияет.
Брейкпоинты с действием `{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки
машины) в трёх точках: вход клавиатурной ветки трамплина, чтение порта 0x18
внутри drain-цикла, запись make-бита для кода `0x6C`. Скратч-байты — хвост
`ovr_tile[]` (в этом сценарии не используется).
**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до
декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил
пользователь.
| Прогон | make дошло / нажато | overrun |
|--------|---------------------|---------|
| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 |
| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 |
| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 |
**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет
и прерывания разрешены практически всё время, потерь НЕ меньше, а больше.
**Вывод 2: `kbd_raw_poll()` в текущей расстановке бесполезен** — 9/10 и с
ним, и без. Причина понятна задним числом: шесть вызовов стоят В ТЕХ ЖЕ
точках, где прерывания и так разрешены, то есть добавляют ровно то, что
трамплин сделал бы сам. Вызовы из `roomtest.c` убраны; сама функция в libc
оставлена — она корректна и нужна как заготовка под «плотный опрос» (см.
ниже), но в горячем цикле её держать не за что.
**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта
0x18: 5 нажатий Shift+Home должны дать ровно 50 байт (нажатие `E0 F0 12` +
`E0 6C`, отпускание `E0 F0 6C` + `E0 12` = по 10 на цикл). Насчитано **49**
— и ровно один make потерян. То есть до процессора байт не доехал вообще,
декодер тут ни при чём.
**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же
49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина
(1.75 байта за вход). То есть больше сорока процентов импульсов запроса
не были обслужены, и байты копятся в трёхбайтовом FIFO вплотную к его
потолку; одна неудачная пауза — и байт потерян.
### KBD-1: ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью
`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме
`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода:
любая работа разредила бы опрос и испортила замер). Это физический
максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики.
| Прогон | нажатий | make дошло | байт прочитано / ожидалось |
|--------|---------|-----------|-----------------------------|
| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) |
| Shift + Home | **25** | **25** | **250 / 250** |
**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах
за кадр терялся 1 байт из 50. При такой частоте потерь вероятность
случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не
совпадение.
**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно
опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за
60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого.
**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято
работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая
луч. Опрос там стоит ноль и покрывает две трети периода с запасом по
плотности. Остаётся слепым только тело одного accel-блита под DI (до
~650 мкс) — разорвать его нельзя (см. «что НЕ делать»).
**Почему нужна именно такая частота (вопрос «PS/2 же не даёт больше 30
нажатий в секунду»).** Частота опроса определяется НЕ темпом нажатий, а
темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Одно нажатие при
зажатом Shift — это 5 байт подряд (`E0 F0 12`, `E0 6C`), отпускание — ещё 5,
и клавиатура выдаёт их со скоростью провода: 11 бит на байт при ~10–16 кГц
= ~0.7–1.1 мс на байт. Воронка — 3 байта. Значит между двумя вычерпываниями
имеют право прийти максимум два байта, то есть вычерпывать надо не реже чем
раз в ~1.5–2 мс (0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду
переполнит FIFO**, если в эти несколько миллисекунд его никто не разгребает.
Замер это подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости.
В норме разгребает прерывание; опрос понадобился только потому, что ~44 %
импульсов здесь теряется.
**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не
ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md`
§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать:
- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт,
break = 2. Нажатие с шифтом перестаёт превышать FIFO в принципе.
- либо хотя бы отключить typematic (`0xF5`/`0xF7`).
**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только
направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь
(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда.
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
кандидат на «когда дойдём до реального железа», а не на сейчас.
**Что сделано по этому плану (2026-08-01):**
1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
нужен push/pop, а сам таймаут в итерациях станет длиннее по времени.
**Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда,
где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60. Полезной
работы не отнимается нисколько.
**Ограничитель области, если «постоянный опрос» всё равно не нравится:**
потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift
потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt —
тогда опрос работает исключительно в той ситуации, ради которой заведён.
2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет.
3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило.
Держать в уме, если на реальном железе или на более тяжёлых сценах
(несколько стражей) потери появятся снова — накрыть блиты дешевле, чем
изобретать что-то новое.
4. ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые
нажатия ровнее человеческих, и «залипания до отпускания Shift» они не
воспроизводили с самого начала.
**Куда смотреть дальше, если плотного опроса не хватит.**
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
его машине занимает часы). Для протокола, подозрение осталось:
`sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта
CPU**, и `m_irq_off_timer`**один на два источника** (`irq_on()` экрана
заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое
прерывание способно обрезать клавиатурный импульс — правдоподобное
объяснение «44 % пропущенных импульсов».
2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе
проблемы может не быть вовсе. Проверять при первом прогоне на живом
Sprinter.
**Про совпадение кадрового и клавиатурного прерываний** (вопрос
пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором
`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл,
значит экран»; совпадение возможно, но «исключительно редкий случай»
(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин
делает ровно предписанное. Известный побочный эффект: при совпадении мы
обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на
потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает
повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка,
в KBD-1 не входит.
**Что НЕ делать (проверено, стоило времени):**
- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di``nop` в
`_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в
памяти **уронил машину в перезагрузку**. То есть режим «акселератор
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
недоступен — вопрос закрыт артефактом, а не рассуждением.
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
---
### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01**
> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту,
> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах
> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид
> стоит, стража нет):
>
> | путь | тактов на кадр | кадров в выборке |
> |------|----------------|------------------|
> | клипающее ядро (как было) | **26 200** | 149 |
> | noclip (стало) | **15 848** | 239 |
>
> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при
> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина —
> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то
> есть на той же геометрии и в той же сцене.
>
> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17),
> BANK2 13 792 → **13 676** (116, свободно стало 2708 Б — это тот самый
> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (263),
> BANK4 без изменений.
>
> **Грабли, стоившие двух пересборок** (вынесено в память
> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static
> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое
> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит
> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в
> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном
> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без
> трамплина, как у `pop_sword_draw`).
>
> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за
> решёткой шва» (straddle — там как раз работает клипающий фолбэк) —
> артефактов и следов нет.
**Ниже — исходная постановка задачи (что и почему смотрели).**
**Зачем сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта
стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против
4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая
оставшаяся оптимизация: не переписывание логики, а выбор ядра.
**Что уже правильно** (шаблон, который надо распространить): блиты Кида,
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
`gfx_blit_cols_part_noclip`, иначе в клипающий вариант
(`pop_kid.c:86,591,676`, `pop_gdraw.c:105`).
**Что чинили:**
-`kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с
клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им
уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего
ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий
`pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`).
-`pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`:
**оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный
параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт
`border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки
тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы
не «оптимизировать» повторно.
**Что обязано остаться с клипом** (зафиксировано комментариями в коде):
- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется
по построению;
- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной)
и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`;
- борта поля (`pop_room_clip_borders`) — см. выше.
**Что уже было правильно** (шаблон, который и распространили): блиты Кида,
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip
БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по
фиксированным координатам; это тоже помечено в коде.
---
## P1 — сразу после P0 (закрываем уровень 1 как ИГРУ, а не стенд)
### L1-START. Старт по данным уровня — **СДЕЛАНО 2026-08-01**
Старт и оба рестарта (смерть, выпадение из уровня) сведены в один
`pop_start_level()` — порт `start_level` + `do_startpos` + `set_start_pos`
(seg003): комната/тайл/направление берутся из `pop_level_start_*`, направление
инвертируется (`~start_dir`), поза входа — из `tbl_entry_pose`. У уровня 1
это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что
захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение
на row 1 — как по данным уровня.
`#define ROOMNAV` оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в
оригинале не было, — как и `S` (выдать меч), `K`, `I`. Все они со временем
съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01).
### L1-EXIT. Выход с уровня (дверь уровня) — **СДЕЛАНО 2026-08-01**
Портированы: ветка двери уровня из `up_pressed` + `go_up_leveldoor`
(seg005:0482/0574) — в `pop_leveldoor_enter()` (`pop_map.c`, тайлы и
геометрия) и `up_pressed()` (`pop_ctrl.c`, только последовательность);
опкод `0xF1 END_LEVEL` в `play_seq` теперь инкрементит `pop_next_level`
(порт `next_level`), а главный цикл по нему перезапускает уровень — ровно
та точка, куда `levels_plan.md` §2.2 подключит загрузку уровня 2.
Открытость двери проверяем по `modifier >= 42` (ветка `fix_exit_door`), а не
по ванильному `leveldoor_open`: с ванильным условием можно войти в ещё
ползущую створку.
**Грабли, которые стоили отдельного разбора:** `go_up_leveldoor` сначала
писал `Char.x`/`Char.direction`, и оба присваивания молча терялись — окно
`Char` вокруг диспетчера возвращает назад ТОЛЬКО `curr_seq` и `sword`
(`pop_savekid_state`). Направление оставалось «вправо», а все `DX`
последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево.
Вывод на будущее: геометрию персонажа в этом порте меняет `pop_map` (пишет в
`Kid`), а не диспетчер.
✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком
проёма) — недоставало второй половины `clip_char` (`obj_clip_right`) и
обрезки СПРАВА у колоночного блита. В libbgi добавлен
`gfx_blit_cols_part_w(..., maxw)`; разбор — в [`bug_closed.md`](bug_closed.md).
### L1-TRIAGE. Ревизия багов — **ЧАСТЬ 1 СДЕЛАНА 2026-08-01**
**Три Critical'а прогнаны в MAME и закрыты** (протокол с числами — в
[`bug_closed.md`](bug_closed.md), раздел «Проверено в MAME 2026-08-01»):
BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2
(ping-pong при возврате) **не воспроизводятся**, BUG-3 (окклюзия climb-up на
кнопке) закрыт фиксом `tile_code_drawn` от 2026-07-28. Заодно снят неверный
диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное
поведение `goto_other_room` (`SDLPoP/src/seg002.c:390` меняет только `x`).
Список разделён на [`bug_list.md`](bug_list.md) (открытое) и
[`bug_closed.md`](bug_closed.md) (закрытое + разбор корней).
**Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были
починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile`
(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty`
(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`),
разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile`
(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md).
**Осталось:** закрыть таблицу обхода 24 комнат
([`bug_list.md`](bug_list.md#обход-всех-24-комнат-уровня-1)) — заполнена на
5 строк из 24, инструмент (`ROOMNAV`) готов. Делать вместе с L1-PASS: это
единственный оставшийся источник новых багов уровня 1.
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()`
= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости
эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою.
Условие «делать ПОСЛЕ CLIP-1» **снято** — CLIP-1 закрыт (см. P0). Проверка —
секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз».
### <a id="l1-pass"></a>L1-PASS. Сквозное прохождение уровня 1 — **SMOKE ПРОЙДЕН 2026-08-05**
> **Прогон 2026-08-05 (пользователь): успешный.** От старта до выхода с
> уровня одним заходом — меч подобран, **оба стража побеждены в честном бою**
> (без читов), выход отработал корректно. Это smoke: пройдены не все
> комнаты, поэтому таблица обхода 24 комнат остаётся открытой. Ценность
> прогона в том, что он снял главные риски этапа 1 разом — боёвка,
> предметы, переход с уровня — и стал регресс-базой для уровня 2.
От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота,
пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно
регресс-база для уровня 2.
Совмещать с [таблицей обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1):
известные баги закрыты, значит новые придут только отсюда. Точки, где стоит
смотреть внимательно, — уже закрытая косметика окклюзии (потолок при прыжке
вверх, шов при анимации решётки, грани дальней колонны) и подъём на
тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md).
### L2. Переход на уровень 2 и его игра — **МАШИНЕРИЯ СДЕЛАНА 2026-08-04**
Порт `levels_plan.md` §2 (шаг 1). Что появилось:
- **Номер уровня стал состоянием.** `pop_current_level` (порт
`current_level`) в `pop_level.c`; `pop_next_level` больше не флаг, а
НОМЕР — `END_LEVEL` его инкрементит (как `++next_level`, seg006:662), а
главный цикл срабатывает по расхождению `pop_next_level !=
pop_current_level` (порт `play_level_2`, seg003:0386).
- **Загрузка по номеру** — `pop_level_load_num(n)`: `LEVELS\res20NN.bin`
(fallback `a:\`), старая EMM-страница отпускается ТОЛЬКО после успешной
загрузки новой (нет файла — играем дальше на текущем). На диск кладутся
все 15 уровней (34 КБ).
- **Потабличные различия** (`data.h:840..848`) — таблицы по 16 в
`pop_level.c`: `tbl_entry_pose` (поза входа), `tbl_guard_hp` (HP стража,
ушло из хардкода `3`), `tbl_guard_type` (**−1 = стражей нет**, иначе на
14/15 они полезли бы из данных комнат), `tbl_level_type` (тайлсет —
пока только читается).
- **`find_start_level_door`** (seg003:02E6) — на уровне 2 это НЕ косметика:
стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина двери
уровня, и без `modif = 43` + `add_trob(...,3)` Кид материализуется внутри
глухой створки. Тип 3 = «быстро закрыть»: дверь захлопывается за спиной
за три кадра, как в оригинале.
- **HP через уровень** — `hitp_beg_lev` (seg003): рестарт уровня
откатывает HP к нему, пройденный уровень подтягивает его к `hitp_max`.
Заодно реализована **большая склянка** (`add_life`, тип зелья 2: +1 к
ПОТОЛКУ HP до 10) — на уровне 2 она есть, комната 20.
- **Меч** — `have_sword = level >= 2` (play_level, seg003:106), а не
жёсткий ноль.
- **Чит Shift+L** (seg000:698) — `pop_next_level = pop_current_level + 1`.
NB: `L` без Shift занят отладочным «осторожным шагом вправо»
(`pop_ctrl` `KBD_DBG_STEPR`); с Shift шаг тоже пройдёт, но уровень тут же
сменится — на практике не мешает.
**Проверено в MAME (2026-08-04):** старт уровня 1 → Shift+L → уровень 2
рисуется правильно (комната 5, большие колонны — новые для нас тайлы 8/9 —
на месте, дверь захлопнута, HP 3) → влево в комнату 4, страж на месте →
Shift+L → уровень 3 (комната 9). То есть цепочка загрузок работает
повторно, а не только один раз.
**Что осталось по уровню 2 (не машинерия, а контент):**
- сквозное прохождение уровня 2 руками — приёмка, как L1-PASS;
- выход через дверь уровня 2 (комната 23) вживую: путь тот же, что уже
закрыт на уровне 1 (L1-EXIT), но не прогнан на этом уровне;
- цвет стража из данных (`guards_color`, на уровне 2 их три разных) — у нас
один атлас; отдельная задача, к переходам отношения не имеет.
### <a id="dbg-cheats"></a>DBG-CHEATS. Отладочные читы SDLPoP — разведка сделана, код не написан
Записано 2026-08-03 (по ходу охоты за BUG-GATE-PASS-1, отложено на потом).
Мотив прямой: мост MAME **теряет нажатия при быстрой отправке**, поэтому
подогнать Кида в нужную позу для отладки автоматикой сейчас нельзя — именно
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
**Берём (дёшево и бьёт в наш класс багов):**
- **`[` / `]` — сдвинуть Кида на пиксель влево/вправо.** Оригинал —
`../SDLPoP/src/seg000.c:1828`:
```c
if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x;
else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x;
```
У нас: коды PS/2 set 2 `[` = **0x54**, `]` = **0x5B** в `pop_cheat.h`
рядом с `KBD_CHEAT_KILL/IMMO/SWORD`; обработка — в том же блоке читов
`roomtest.c` (~строка 479), **по фронту** (`*_prev`, как у остальных),
иначе одно нажатие уедет на десяток пикселей. Работает по `Kid.x`
напрямую: геометрию персонажа в этом порте меняет не диспетчер (см.
грабли L1-EXIT).
Первый же потребитель — BUG-GATE-PASS-1: там надо снять `Kid.x` и
`Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё стоит
на её тайле.
- **Shift+L — следующий уровень.** Уже входит в L2-машинерию
(`../docs/levels_plan.md` §2.4): `++pop_next_level` и всё.
**Остальные — оценка, а не обязательство:**
| Чит | Вердикт |
|-----|---------|
| **T — таймер** | **не сейчас**: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой |
| **F — остаток feather-fall** | **вместе с Shift+W**: ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно |
| **Shift+F9 — quickload с тем же уровнем** | **самое ценное и самое дорогое**: это сериализация `Char` + `room_modif` всех комнат + trob'ов + стражей (`levels_plan.md` §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки. Кандидат сразу после того, как заработают уровни |
---
### <a id="mem-bank5"></a>MEM-BANK5. Разгрузка W1/W2 новым банком кода
Вопрос 2026-08-04: «надо делать новый банк?». **Да, и он лечит именно то,
что жмёт.** В нашей раскладке (`MEMORY=huge`, small-вариант) CODE и DATA
живут в ОДНОМ 32-КБ пространстве W1+W2 — карта текущей сборки:
```
_CODE 0x4100..0xAA00 26880 Б
_DATA 0xAAD0..0xB9B0 3808 Б
_BSS 0xB9B8..0xBADA 290 Б
куча 0xBADA..0xBB00 38 Б ← упёрлись сюда, добавляя отладку
стек 0xBB00..0xC000 1280 Б
```
Поэтому **каждый килобайт кода, уехавший в банк, становится килобайтом,
доступным данным**. Отдельного «дефицита W2» у нас нет — дефицит один.
(38 байт кучи не опасны сами по себе: malloc'ом мы не пользуемся, страницы
берутся через `mem_alloc_block`. Опасно то, что следующая структура
данных упрётся в стек молча.)
Резидентный код по модулям (из `.sprinter-cc-roomtest/*.rel`):
| модуль | _CODE | как часто зовётся | в банк? |
|--------|------:|-------------------|---------|
| `pop_kid.c` | 6287 | `load_frame`/`play_seq` — 2×/кадр (Кид + страж) | частично: холодная половина (загрузка страниц спрайтов, `pop_kid_load`) — да; движок кадров — нет |
| `roomtest.c` | 3893 | main-loop | нет (точка входа, зовёт всех) |
| `pop_trob.c` | 2526 | `do_trobs` 1×/кадр, но `pop_trob_modif` — горячий аксессор | **кандидат №1**, если вынести аксессор в резидент |
| `pop_level.c`| 2292 | `pop_level_tile` — из `pop_bg` (банк 2) на каждый тайл | **нет**: банк→банк на каждый тайл убьёт отрисовку |
| `pop_ctrl.c` | 2189 | `user_control` 1×/кадр | **кандидат №2** — дёшево и безопасно |
| `pop_guard.c`| 923 | 1×/кадр | нет смысла |
Порядок действий, когда упрёмся: `pop_ctrl.c` → банк 5 (2.2 КБ, один
banked-вызов за кадр), затем расщепление `pop_kid.c` на горячее ядро и
холодную загрузку. Критерий кандидата — **не размер, а частота вызова и
отсутствие горячих банк→банк переходов**; `pop_level` показывает, что
большой холодный на вид модуль может быть горячим аксессором.
Брать по факту нехватки места, не заранее.
---
## Отложено осознанно (не брать, пока не появится причина)
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6.
- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти
бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается.
- **Отключение мыши на время игры** и **замена PRNG** —
`../docs/ideas_backlog.md` (оба дают доли процента кадра).
- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость
транзиентная; разбор в [`bug_closed.md`](bug_closed.md).
+47
View File
@@ -0,0 +1,47 @@
/*
* _pop_draw.h внутренний заголовок слоя отрисовки: выбор ядра libbgi
* (клипающее / линейное «noclip») по одному и тому же тесту для блита и
* для heal.
*
* ПОЧЕМУ ОТДЕЛЬНЫЙ ФАЙЛ, А НЕ pop_bg.h. Обе функции `static inline`, а
* SDCC 4.5 оставляет тело такой функции в КАЖДОМ TU, который видит
* объявление, даже если тот её не зовёт (memory
* `sdcc_inline_codegen_findings`). pop_bg.h включают почти все модули PoP,
* то есть из общего заголовка эти ~100 Б размножились бы десятком мёртвых
* копий (замер CLIP-1, 2026-08-01: +1091 Б в _CODE и +636 Б в банке
* стража за код, который там никто не вызывает). Здесь их видят ровно
* три файла, которые реально рисуют: pop_bg.c, pop_kid.c, pop_gdraw.c.
*/
#ifndef POP_DRAW_H
#define POP_DRAW_H
#include <stdint.h>
#include <gfx.h>
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
* того, вылезает край или нет. */
static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h)
{
return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 &&
x + (int)w <= 320 && y + (int)h <= 256);
}
/* Стереть прямоугольник (heal из ОЗУ-копии) тем же приёмом, что и блит:
* целиком на экране линейное ядро без клипа. Условие входа у
* gfx_heal_noclip ровно то же, что у gfx_blit_cols_part_noclip, поэтому
* тест один общее ядро gfx_heal платит за клип и 16-бит, а не за пиксели
* (замер: 11 658 тактов на heal 22×22).
*
* НЕ inline (тело pop_draw.c, резидент W1): SDCC 4.5 встраивал бы его в
* каждое место вызова по 181 Б И оставлял мёртвую копию в каждом TU см.
* шапку pop_draw.c с замером. Зовётся 5 раз за кадр, цена вызова тонет в
* стоимости самого heal.
*
* w/h тут int, а не uint16_t: вызывающие считают их вычитанием (h skip), и
* отрицательный результат обязан быть no-op, а не превратиться в огромный
* unsigned. */
void pop_heal_fast(int x, int y, int w, int h);
#endif
File diff suppressed because it is too large Load Diff
+413
View File
@@ -0,0 +1,413 @@
# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней)
переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
грепни там по симптому.
Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта:
механику сверять с `../SDLPoP/src/` ДО кодинга.
Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1). Одиннадцать
наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены
в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень
стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень),
BUG-DEATH-1 (смерть от меча), BUG-GATE-ANIM-1 (анимация ворот), BUG-COLL-1
(полный порт `check_collisions`), BUG-STANDUP-1 (лишний guard в
`bumped_floor`). Клавиатурный BUG-KBD-3 доведён до конца во второй волне
(см. ниже); из первой волны открытым остался косметический BUG-SEAM-DRAW-1.
Ревизия **2026-08-03, вечер** (вторая волна с прогона): все четыре наблюдения
закрыты — BUG-KBD-4 (фантомный отжим Shift), BUG-RESPAWN-2 (стражи не оживают),
BUG-DRAWORDER-1 (Кид поверх тела стража) и BUG-LOOSE-2 (осколки только от одной
из двух плит). У BUG-DRAWORDER-1 остался артефакт «ноги поверх головы»,
сверенный с SDLPoP и записанный в «НЕ БАГИ». Тогда же закрыт как
невоспроизводящийся BUG-SEAM-DRAW-1 (Кид в шве рисуется) и заведён
BUG-GATE-PASS-1 — однократное наблюдение прохода сквозь закрывшуюся решётку,
которое ЖДЁТ надёжного сценария.
| ID | что | тип | статус |
|----|-----|-----|--------|
| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария** (BUG-GATEMOD-1, из-за которого решётка стартовала не в том состоянии, закрыт — перепроверить) |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается |
---
<a id="уровень-2"></a>
# Уровень 2 — баги с приёмки (заведено 2026-08-04)
Уровень 2 заиграл 2026-08-04 (см. [`TASKS.md`](TASKS.md) запись **L2**).
Пользователь при первом прогоне увидел **несколько багов отрисовки** и
подаст их списком отдельно — раздел заведён под них заранее, чтобы
наблюдения не растеклись по переписке.
**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md#bug-loose-3) — чёрный
бар под упавшей плитой-потолком (запечка восстанавливала не всё, что стёрла).
**Порядок работы по уровню 2 (2026-08-05):** наблюдения СНАЧАЛА копятся
здесь целиком — фиксы берём, когда уровень пройден до конца. Причина:
косметика отрисовки почти всегда сводится к общему корню (ср. волну
BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит
чинить один и тот же корень трижды.
Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие
кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass),
запись **L2-PASS**: по ней видно, «механика не сработала» это или
«так и задумано».
Перед заведением записи — грепнуть [`bug_closed.md`](bug_closed.md) по
симптому: почти вся косметика окклюзии уровня 1 уже разбиралась
(BUG-CEIL-1/2/3, BUG-OCCL-1, BUG-DOOR-CLIP, BUG-DRAWORDER-1), и уровень 2
вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9)
и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый.
<a id="уровень-3"></a>
# Уровень 3 — баги с приёмки (заведено 2026-08-05)
Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два
из них — **не баги, а неначатые задачи**, и записаны как таковые:
| наблюдение | что это на самом деле |
|------------|------------------------|
| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта |
| к.10: скелет не оживает | [L3-SKEL](TASKS.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован. **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 |
Остальные два были настоящими багами и **закрыты в тот же день** — разбор
корней в [`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** (флаги
коллизий считались для одного ряда вместо трёх — бамп терялся на кадре
смены ряда) и **BUG-SEAM-WEDGE-1** (сосед угла (2,0) безусловно считался
стеной).
<a id="bug-spike-1"></a>
## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
> **Статус 2026-08-05, вечер.** Пользователь **повторить не смог**, а замер
> (ниже) показал, что чистый пробег по убранным пикам убивает штатно. То
> есть в смертельности бага, похоже, нет вовсе: наблюдался частный случай —
> пики, УЖЕ выдвинутые полностью (h = 1), для бегущего безвредны и в
> оригинале. Запись оставлена открытой ровно из-за визуального расхождения
> со скриншотом SDLPoP (у нас острия торчат, у него убраны) — см. конец.
>
> **Как получить состояние нарочно:** подойти к пикам вплотную (выдвинутся),
> отступить на полшага, НЕ выходя из зоны срабатывания, и пробежать по ним.
> Пользователь пробовал и это, и попиксельную подгонку X читом `[`/`]`
> не поднялось. Вывод для будущего разбора: состояние **не чисто
> позиционное**, одной шириной габарита его не объяснить; следующий
> подозреваемый — момент, в который `process_trobs` застаёт модификатор
> относительно кадра Кида.
**Наблюдение (пользователь, 2026-08-05).** Уровень 2, комната 6, пики (1,3):
| действие | что происходит |
|----------|----------------|
| длинный прыжок с ряда 0 на пики | **смерть — правильно** (путь `fell_on_spikes`) |
| пробег по ряду 1 прямо по пикам | **урона нет** |
| после уборки пик | **на экране остаются белые остатки остриёв** (в оригинале чисто) |
| прыжок на месте, стоя на пиках | урона нет |
| просто стоять на выдвинутых пиках | можно сколько угодно |
**Замер (MAME, чтение `room_modif` комнаты 6).** Пока Кид стоит на тайле,
модификатор пики (индекс 13) = **0x8E**, то есть «пики ПОЛНОСТЬЮ вышли и
идёт обратный отсчёт». Дальше вся арифметика сходится с оригиналом:
```
is_spike_harmful (seg007:1178): 0/-1 → 0; <0 → 1; 1..4 → 2; >=5 → 0
check_spiked (seg006:0658): убивает при h>=2 на кадрах бега 7..14
и при h!=0 на кадрах приземления 43/26
```
То есть **при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале**
смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2). Наши
`animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked`
сверены с seg006/seg007 построчно и совпадают дословно.
**Значит расходится ТАЙМИНГ, а не логика:** у нас пики успевают полностью
выйти ДО того, как Кид на них вбежит, и он проскакивает мимо смертельного
окна 1..4. Триггер — `check_spike_below` (seg006:1199), и у нас он берёт
габарит спрайта из `kid_fp` — метрик ПОСЛЕДНЕГО отрисованного кадра, а
оригинал берёт `char_x_left/right` ТЕКУЩЕГО кадра (`set_char_collision`).
Это первый подозреваемый; второй — момент вызова `process_trobs`
относительно `play_frame` (сколько кадров модификатор успевает пройти,
пока Кид добегает).
**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом) — оно и уточняет
гипотезу.** Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и
«спрятались не все — часть артефактов осталась». Скриншоты рядом:
[наш](bugscreens/l2-r6-spikes-ours.png) и
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под
щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.
Это стыкуется с замером: пока Кид стоит рядом, `check_spike_below` каждый
кадр зовёт `start_anim_spike`, а тот при отрицательном модификаторе
переставляет его обратно в 0x8F — то есть пики у нас «залипают выдвинутыми»
и отсчёт до уборки не доходит. В оригинале в той же позе они убраны,
значит его `check_spike_below` эту колонку УЖЕ не задевает.
**ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер
2026-08-05, MAME, watchpoint на `room_modif[13]` комнаты 6).** Чистый
пробег по УБРАННЫМ пикам убивает штатно:
```
запись modif=1 : кадр 11 (беговой), x=112, curr_col=2 ← пики пошли вверх
запись modif=2 : кадр 12 (беговой), x=117, curr_col=3 ← Кид уже НА тайле, h=2
запись modif=3 : кадр 177 (frame_177_spiked) ← напоролся
```
То есть `check_spike_below`, `check_spiked`, `is_spike_harmful` и тайминг
выдвижения работают правильно, и «раннего» триггера нет.
**Настоящий корень — пики ЗАЛИПАЮТ выдвинутыми.** Пока габарит Кида
накрывает колонку пики, `check_spike_below` каждый кадр зовёт
`start_anim_spike`, а тот при отрицательном модификаторе переставляет его
обратно в 0x8F — отсчёт до уборки не доходит. А выдвинутые пики (h = 1)
для бегущего БЕЗВРЕДНЫ по правилам оригинала. Отсюда обе жалобы: пробег
по уже вышедшим пикам не убивает, и они же остаются торчать на экране.
**Что осталось выяснить (и это единственный открытый вопрос).** Код
`start_anim_spike` у нас с оригиналом совпадает дословно, значит оригинал
тоже удерживал бы пики, стой Кид там же. На скриншоте SDLPoP в похожей
позе пики УБРАНЫ — то есть его Кид стоит чуть левее и его габарит колонку
пики уже не задевает. Разница в 2–3 пикселя посадки, а у нас такие
расхождения по X уже ловились (см. заметку в BUG-GRAB-1: после касания
площадки SDLPoP уводит Кида на 134, мы — на 141).
**Как закрывать:** инструментировать SDLPoP (печать `char_x_left/right`,
`left/right_checked_col` и модификатора пики каждый кадр), проиграть ту же
сцену — падение плиты-потолка в комнате 6 и остановку на щебне — и сверить
с нашей трассой ПОЗИЦИЮ КИДА после приземления. Если позиции совпадут, а
диапазоны колонок разойдутся — виноват габарит (`kid_fp` против
`set_char_collision` текущего кадра); если разойдутся позиции — это отдельный
баг посадки, а пики — его следствие.
---
<a id="ручная-перепроверка-2026-08-03"></a>
# Ручная перепроверка фиксов (2026-08-03)
Шесть корней закрыты автоматической проверкой в MAME (мост `mame-z80`:
чтение `_Kid`, потиковые трассы, скриншоты) — этого достаточно, чтобы
показать, что конкретный сценарий больше не воспроизводится, но НЕ
достаточно, чтобы поймать регрессии в соседней механике. Ниже — сценарии
ровно в тех формулировках, в которых баги были заведены.
Собрано и лежит в образе (`make` + `make hdd` уже прогнаны; libc пересобран,
`make size-check` без роста).
| # | что проверить | ожидаемо |
|---|---------------|----------|
| 1 | комната 22: выпить зелье (0,6), выйти в 16/23 и вернуться | кувшина нет, пузырька над пустым местом нет |
| 2 | то же для комнат 14 (0,5) и 17 (2,3) | так же |
| 3 | комната 15: подобрать меч (2,2), выйти и вернуться | меча нет; кладка на дальней стене НЕ мигает |
| 4 | комната 12: разбить плиты (0,1)/(0,2) и потолок в 16, выйти-вернуться | остаются разбитыми, проём не закрывается |
| 5 | комната 17 из 23: разбить (1,5)/(1,6), выпить зелье (2,3), вернуться | всё остаётся |
| 6 | **после смерти** зайти в те же комнаты | ВСЁ восстановлено (в оригинале смерть = `load_level`) |
| 7 | комната 12: разбег в закрытую решётку (0,9) с полушага | не проходит насквозь; перелистывание экрана штатно (см. BUG-SEAM-DRAW-1 в `bug_closed.md`) |
| 8 | комната 6: бег справа налево от (0,9), длинный прыжок (0,6)→(0,7) | не влетает внутрь стены |
| 9 | комната 5: с кнопки (0,6) падение на (2,7), присед, вставание | остаётся в комнате 5 (проверено трассой: `fr=111 x=177``seq_47``fr=15 x=173`) |
| 10 | комната 5: нажать кнопку (0,4) | поднимаются ОБЕ решётки — (0,5) видно на экране, (0,9) проверять из комнаты 1 |
| 11 | страж (комнаты 3, 21) убивает Кида | смерть доигрывается, респавн в стартовой позиции уровня; цикла «убил-воскрес» нет |
| 12 | клавиатура: долгая игра с Shift+стрелка | ↑ и Shift не залипают; осторожный шаг не превращается в бег (доработано во второй волне — п.17) |
## Вторая волна (2026-08-03, вечер)
| # | что проверить | ожидаемо |
|---|---------------|----------|
| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки |
| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные |
Отдельно стоит посмотреть **на регрессии от порта `check_collisions`** — он
трогает всю горизонтальную коллизию: бамп в стену на бегу и в прыжке,
осторожный шаг у стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме
решётки, переходы через швы (старый BUG-SEAM-PINGPONG).
---
# Открытые баги уровня 1
<a id="bug-gate-pass-1"></a>
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ
**Статус: наблюдался один раз, воспроизвести повторно НЕ удалось.** Заведён,
чтобы наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
«не баг», пока нет надёжного сценария.
**Что наблюдалось (2026-08-03).** Комната 5: Кид стоял НА тайле решётки (0,9)
и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1
и упал на (1,1).
**Что уже измерено и в чём загвоздка.** Сразу после наблюдения повторить не
получилось: в том же месте Кид стоит на `x = 196`, `col = 9`, и решётка его
ДЕРЖИТ — то есть штатно.
Арифметика оригинала объясняет разницу. `is_obstacle` (seg004) ставит
плоскость блокировки в `x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX`, для
колонки 9 это **x = 205**. При этом «колонка 9» по `get_tile_div_mod_m7`
это `x ∈ [191, 205)`. Пока `curr_col == 9`, Кид гарантированно левее
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться **правее
205**, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
весь тайл.
Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
по тайлу дальше, пока решётка была поднята), и поведение штатное. Но повторить
эту позу и снять `x` пока не удалось, поэтому гипотеза НЕ подтверждена.
**Что снять в следующий раз** (без этих чисел вопрос не закрыть):
1. `Kid.x` и `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё
стоит на её тайле — до шага вправо;
2. модификатор решётки (openness) комнаты 5, тайл 9 — `can_bump_into_gate()`
считает её препятствием только пока `(modif >> 2) + 6 < char_height`, то
есть пока она опустилась достаточно низко относительно РОСТА кадра;
3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.
Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
продолжить), затем чтение `_Kid` из отладчика MAME.
**Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
(ср. закрытый BUG-SEAM-PINGPONG). Второй кандидат — `char_height` в
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени.
# Оптимизация отрисовки (записано 2026-07-29)
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
кадр), а у нас каждая такая пометка превращается в реальный heal (копию из
ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем
безусловно» корректен, но дорог.
<a id="t-1"></a>
## T-1. Пики: перерисовывать по причине, а не безусловно
**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
каждой живой пики в комнате (порт `redraw_21h`, который `animate_spike`
вызывает вне всяких `if`). Это корректно, но лишнее для пик, до которых
Киду дела нет.
**Надо:** перерисовывать тайл пики, только если
1. **сменился её видимый кадр** (шаг выдвижения/уборки), ЛИБО
2. **её кто-то стёр** — а стереть у нас может только heal, то есть тайл
попал в прямоугольник `kid_heal` этого кадра.
Это и есть модель оригинала, просто выраженная флагами: `redraw_at_char`
(seg003:0576) каждый кадр помечает `set_redraw_fore` тайлы персонажа, причём
**объединение текущего и предыдущего** прямоугольника
(`MIN(char_top_row, prev_char_top_row)` и т.д.), а `animate_spike` помечает
свой тайл. Итог = {тайл сменил кадр} ∪ {тайлы Кида}.
**Как:** слой Кида и так считает `cL..cR`/`rT..rB` в `pop_fore_over_kid`
пусть публикует их (плюс предыдущие, как в оригинале), а цикл trob'ов
сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений
прямоугольников (см. память `manual_hints_over_auto_detect`).
**Приоритет:** отдаётся почти бесплатно ПОСЛЕ T-2, отдельно не окупается.
<a id="t-2"></a>
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит
**Сейчас:** `kid_heal``kid_draw``pop_fore_over_kid` идут каждый кадр,
даже когда Kid стоит и в его тайлах ничего не меняется. Это ровно поведение
оригинала (`draw_game_frame`, seg000:917 — `draw_moving()` + `draw_tables()`
безусловно), но у него это дёшево, а у нас нет.
**Надо:** пропускать heal+draw Кида, когда кадр/поза/координаты не менялись
и в его тайлах нет активной анимации.
**Осторожно (дабл-буфер):** пропускать можно **не раньше второго подряд**
неизменного кадра — иначе одна из двух страниц останется со старым
содержимым. Условие «обе страницы уже получили это состояние».
**Связь с T-1:** после T-2 пики отпадают сами — раз Кида не перерисовываем,
heal'а нет, стирать пики нечем, редрой не нужен.
**Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно
там, где тапают Shift+стрелку (см. остаток KBD-1 в [`TASKS.md`](TASKS.md)).
---
# Обход всех 24 комнат уровня 1 — поиск багов отрисовки
Инструмент: `#define ROOMNAV` в `roomtest.c``+`/`-` (цифровой блок либо
`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой),
Kid ставится на первый пол, `pop_trob_reset()` возвращает пики/ворота в
исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа
единицы (`||` `||||` = 24). Убирается закомментированием одной строки.
Комнаты **13, 18, 24 недостижимы** в обычной игре — это свойство данных
уровня, разбор в [`bug_closed.md`](bug_closed.md) («НЕ БАГИ»). Приоритет
багов в них низкий.
Таблица заполнена на 5 строк из 24 — доделать в рамках L1-TRIAGE
([`TASKS.md`](TASKS.md)).
| комната | статус | что не так |
|---------|--------|------------|
| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты |
| 2 | | |
| 3 | | |
| 4 | | |
| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` |
| 6 | | |
| 7 | | |
| 8 | | |
| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) |
| 10 | | |
| 11 | | |
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» в `bug_closed.md` |
| 13 | недостижима в игре | |
| 14 | | |
| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
| 16 | | |
| 17 | | |
| 18 | недостижима в игре | |
| 19 | | |
| 20 | | |
| 21 | | |
| 22 | | |
| 23 | | |
| 24 | недостижима в игре | |
---
## Заметки (отладка)
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
сторону = разворот (как в оригинале safe_step), движение со второго.
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
**S** — выдать меч.
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в
настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
---
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
## Сырые наблюдения (прогон 2026-08-03) → корень
Формулировки с прогона, как записаны. Закрытые корни — в
[`bug_closed.md`](bug_closed.md).
| # | наблюдение (кратко) | корень | статус |
|---|---------------------|--------|--------|
| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт |
| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт |
| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт |
| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт |
| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт |
| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт |
| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) |
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) |
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт |
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт |
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт |
+80
View File
@@ -0,0 +1,80 @@
Активные баги на уровне 1 -
- После того как кувшин со здоровьем выпит и Кид попадает в комнату во второй раз
то иногда кувшин мы не показываем (это правильно) но показываем анимацию пузырьков над
кувшином (чего показывать больше не должны - нет кувшина, нет пузырьков),
а в комнате 14 пузырек вообще восстанавливается полностью после того как Кид
выйдет из комнаты и вернется в нее обратно (есть ощущение что даже в одной комнате
поведение пузырька - будет ли он восстановлен или нет зависит от чего-то непонятного.
(есть еще ощущение что первый выпитый кувшин не восстанавливается а остальные
восстанавливаются)
- Меч - один раз подобранный он больше не должен появляться в своей (15-ой) комнате
иногда он появляется снова, иногда он больше не появляется но на дальней стене начинают
моргать контуры кирпичной кладки (странно что она не впечатана в heal уровень потому
что она должна быть отрисована всегда). причем мерцает по странному - не как double-buff
а как буд-то вместо искры на мече - изредка на несколько (один логический?) кадров.
- комната 12 - опять проблема с телепортацией через решетку - решетка в 0,9 - Кид стоит
за шаг (пол-шага) от решетки - бежит и пробегает через закрытую решетку.
- после respawn - остаются разбитыми плиты что были разбиты, выпитыми пузырьки и прочее -
это надо сравнить с оригиналом (SDLPoP) - но главное другое - если до respawn какая-то
плита не была разбита то теперь если ее разбить, выйти из комнаты и вернуться - то
плита восстановится (еще интереснее вариант когда плита-потолок в комнате 12 разбивается,
Кид через проем поднимается в верхнюю комнату и поднявшись плита в новой комнате опять
закрывает проем.
- проблема с плитой из комнаты 12 в верхнюю и пузырек в комнате 22 - они возникают даже
без respawn - когда Кид заходит в комнату 22 из комнаты 16, выпивает пузырек, выходит опять
в комнату 16 и опять возвращается в комнату 22 - пузырек возникает снова. Когда Кид разбивает
плиту из комнаты 12 в комнату 16 то после подъема в комнату 16 плита опять возникает.
аналогично комната 17 - если войти в нее из комнаты 23, разбить плиты 1,5 и 1,6, опуститься на
2,3 и выпить пузырек. вернуться в комнату 23 и опять в 17 - то и плиты вернутся и пузырек.
но могут плита и пузырек не вернуться - такое ощущение что если это первая разбитая плита
(Кида мы перенесли в комнату 12 через cheats) - то разбитая плита остается разбитой.
- комната 6 - Кид бежит по уровню ноль справа налево (начало бега 0,9), делает длинный
прыжок между 0,6 и 0,7 - он пролетает во внутрь стены и респавнится - скриншоты в bugscreens
- иногда залипают все-таки клавиши (пока замечено на кнопке вверх - отлипает только после
следующего нажатия). надо проверить - есть ли возможность не только отслеживать события
нажатие/отжатие но и текущий статус нажатых кнопок. Может вообще отказаться от прямой работы
с клавиатурой а работать с нею через BIOS ?
- иногда таки залипает и shift =((((
- комната 5 - кнопка на 0,4 не открывает решетку/ворота на 0,5 - это двойная кнопка - она
должна сразу открывать ворота на 0,5 и на 0,9 (возможно у нас она работает только на 0,9)
возможно что логически решетка на 0,5 открывается но анимации ее открытия нету =(
- комната 5 - Кид падает с кнопки 0,6 на 2,7 - получает урон, присяд в 2,8 - когда встает
он проваливается еще ниже в комнату 6 на между 2,7-2,8.
- если стражник убивает Кида то Кид респавнится на месте смерти и стражник тут же его убивает
еще раз и так по циклу
--------
- проверить как отрисовывается Кид вместе с мертвым стражником - в оригинале стражник поверх
Кида (Кид проходит за телом стража), у нас Кид поверх (перед) стражем;
- сейчас у нас шифт не залипает но теперь он теряет статус нажатости - то есть при нажатом шифте
второе-треть-четвертое нажатие стрелки отрабатывает не как осторожный шаг а переводит Кида на бег
по идее с новым алгоритмом должно было бы корректно отрабатывать нажатие шифта ? теперь же залипаний
нету но есть фантомный отжим ? такое поведение работы с клавиатурой хуже чем залипание шифта (потому
что игрок надеется что шифт нажат и будет короткий шаг а Кид начинает бежать и может провалиться в
ловушку/яму или набежать на пики и погибнуть). то есть надо как-то более точно проверять статус нажатости
шифта.
то есть - когда нажат шифт и нажимаются стрелки то вариант редких пропусков стрелок предпочтительнее чем
вариант когда мы перестаем понимать что шифт нажат.
- после respawn - должны ли оживать стражники ? сейчас они остаются мертвые.
- небольшой баг отображения (такой баг у нас уже был и ты его фиксил) - комната 12 - две падающие плиты
рядом (0,1 и 0,2) - после падения осколки только от одной на 1,2 а от 0,1 на 1,1 осколков нету.
при том что в комнате 7 две плиты 0,5 и 0,6 падая на 2,5 и 2,6 дают двое осколков.
возможная причина - когда Кид пробегает в комнате 12 по падающим плитам то он убегает сразу в комнату 15
и плита 0,2 успевает упасть и поменять статус пола 1,2 на пол с осколками а плита 0,1 еще остается в падении
когда Кид уже уходит в комнату 15 и потому статус пола на 1,1 на 'пол с осколками' измениться не успевает
--------
Подумай вот над чем -
интересует возможность покрытия уже написаного кода автотестами (тесткейсами).
автотесты внутри MAME я думаю будут достаточно затруднительными, но поскольку это у нас plain-C
то можно наверное делать автотесты и на host OS.
возможно это нам поможет не создавать ситуации когда нам каждый раз приходится фиксить баги которые
мы уже фиксили (по крайней мере постараемся избегать регресса при реализации новых фич)
я понимаю что это скорее всего будет достаточно сложно совместить с тем что финальный продукт идет
для Sprinter. но подумать над этим надо.
Пока ничего не предпринимай, только подумай что и как можно сделать.
Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

+502
View File
@@ -0,0 +1,502 @@
/*
* guards.c ЛОГИКА стража, собирается в БАНК (--bank 1=guards.c, окно W3).
*
* Правила банка (проверено пробником tests/w3bankgfx):
* - состояние держим в W1/W2 (pop_guard.c): статики банка линкуются в
* страницу банка и снаружи не читаются;
* - звать можно W1/W2-код (pop_map, pop_kid, pop_geom, libc, примитивы
* libbgi) скобка _bgi_begin/_bgi_end возвращает страницу БАНКА;
* - звать НЕЛЬЗЯ резидент W3 (pop_bg) ни напрямую, ни транзитивно:
* пока исполняется банк, резидентной страницы в адресном пространстве
* нет. Поэтому фон/окклюзию за стража рисует главный цикл.
*
* Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000
* play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой:
* ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а
* дальше работает общий pop_control().
*/
#include "pop_guard.h"
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "pop_geom.h"
#include "pop_trob.h"
#include "pop_state.h" /* pop_immortal — чит бессмертия */
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
#define TILE_GATE 4
#define TILE_DOORTOP_FLOOR 7
#define TILE_LOOSE 11
#define TILE_DOORTOP 12
#define TILE_CHOMPER 18
#define TILE_WALL 20
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его
* через границу банка дороже, чем повторить. */
static uint8_t tile_is_floor(uint8_t t)
{
switch (t) {
case 0: case 9: case 12: case 20:
case 26: case 27: case 28: case 29:
return 0;
default:
return 1;
}
}
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid,
* seg003:761): xpos 7 делится на ширину тайла с округлением вниз. */
static int8_t tile_col; /* колонка последнего tile_at_kid */
static uint8_t tile_at_kid(int16_t xpos)
{
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos 7 58)/14),
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]).
* Оригинал берёт её из tile_div_tbl, у нас делением с округлением
* вниз, как в pop_map. */
int16_t x = xpos - 7 - SCREENSPACE_X;
int16_t col = x / TILE_SIZEX;
if (x % TILE_SIZEX < 0) --col;
tile_col = (int8_t)col;
return pop_tile_at(tile_col, Kid.curr_row);
}
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
* 2 видит и пойдёт. */
void pop_check_can_guard_see_kid(void) __banked
{
uint8_t kid_frame = Kid.frame;
int16_t left_pos, right_pos;
uint8_t tile;
if (!(Guard.charid != 0 &&
/* кадры 219..228 — подъём по лестнице: там страж Кида не видит */
kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) &&
Kid.alive < 0 && Guard.alive < 0 &&
Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) {
can_guard_see_kid = 0;
return;
}
can_guard_see_kid = 2;
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
if (left_pos > right_pos) {
int16_t t = left_pos; left_pos = right_pos; right_pos = t;
}
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
if (tile_at_kid(left_pos) == TILE_CHOMPER)
left_pos += TILE_SIZEX;
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
if (tile_at_kid(right_pos) == TILE_GATE)
right_pos -= TILE_SIZEX;
while (left_pos <= right_pos) {
tile = tile_at_kid(left_pos);
/* Сквозь это не видно вовсе. */
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
can_guard_see_kid = 0;
return;
}
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
* упрётся в неоткрытые ворота / шагнёт в дыру. */
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
can_guard_see_kid = 1;
} else if (tile == TILE_GATE &&
pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) {
can_guard_see_kid = 1; /* ворота подняты не до конца */
}
left_pos += TILE_SIZEX;
}
}
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
static void move_0_nothing(void)
{
control_shift = control_y = control_x = control_shift2 = 0;
control_down = control_up = control_backward = control_forward = 0;
}
static void move_1_forward(void)
{
control_x = POP_CONTROL_HELD_FORWARD;
control_forward = POP_CONTROL_HELD;
}
static void move_2_backward(void)
{
control_backward = POP_CONTROL_HELD;
control_x = POP_CONTROL_HELD_BACKWARD;
}
static void move_3_up(void)
{
control_y = POP_CONTROL_HELD_UP;
control_up = POP_CONTROL_HELD;
}
static void move_4_down(void)
{
control_down = POP_CONTROL_HELD;
control_y = POP_CONTROL_HELD_DOWN;
}
static void move_6_shift(void)
{
control_shift = POP_CONTROL_HELD;
control_shift2 = POP_CONTROL_HELD;
}
static void move_down_back(void)
{
control_down = POP_CONTROL_HELD;
move_2_backward();
}
static void move_down_forw(void)
{
control_down = POP_CONTROL_HELD;
move_1_forward();
}
/* autocontrol_guard_inactive (seg002:710): страж с убранным мечом.
* Пока Кид за спиной страж не реагирует; исключение «шум» Кида
* (is_guard_notice), от которого страж оборачивается. */
static void autocontrol_guard_inactive(void)
{
int16_t distance;
if (Kid.alive >= 0) return;
distance = pop_char_opp_dist();
/* В оригинале условие записано как (word)distance < (word)-8 —
* беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона 8..1.
* То есть: Кид не «вплотную за спиной». */
if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) {
if (is_guard_notice) {
is_guard_notice = 0;
if (distance < 0) {
if (distance < -4) move_4_down(); /* обернуться на шум */
return;
}
} else if (distance < 0) {
return;
}
}
if (can_guard_see_kid) {
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
move_down_forw();
}
}
/* ---- Активный страж (меч обнажён), seg002:737.. -------------------- *
* Таблицы вероятностей по 12 градациям мастерства (seg002:26..38). Бросок
* prandom(255) с СОБСТВЕННЫМ сидом: у нас последовательности разведены
* по доменам (кладка / анимация тайлов / бой), см. pop_geom.h. */
static const uint8_t STRIKEPROB[NUM_GUARD_SKILLS] = { 61,100, 61, 61, 61, 40,100,220, 0, 48, 32, 48};
static const uint8_t RESTRIKEPROB[NUM_GUARD_SKILLS] = { 0, 0, 0, 5, 5,175, 16, 8, 0,255,255,150};
static const uint8_t BLOCKPROB[NUM_GUARD_SKILLS] = { 0,150,150,200,200,255,200,250, 0,255,255,255};
static const uint8_t IMPBLOCKPROB[NUM_GUARD_SKILLS] = { 0, 61, 61,100,100,145,100,250, 0,145,255,175};
static const uint8_t ADVPROB[NUM_GUARD_SKILLS] = {255,200,200,200,255,255,200, 0, 0,255,100,100};
/* Бросок «сработало ли»: в оригинале prob > prandom(255). */
static uint8_t roll(uint8_t prob)
{
return (uint8_t)(prob > (uint8_t)pop_prandom(&pop_fight_seed, 255));
}
/* guard_advance (seg002:0AF5): наступать. Мастерство 0 наступает всегда,
* остальные только пока Кид не замахнулся. */
static void guard_advance(void)
{
if (guard_skill == 0 || kid_sword_strike == 0) {
if (roll(ADVPROB[guard_skill])) move_1_forward();
}
}
/* guard_block (seg002:0B1D): блокировать замах Кида. Если Кид только что
* пробил блок (justblocked) своя, «импровизационная» вероятность. */
static void guard_block(void)
{
uint8_t of = Opp.frame;
if (of == FRAME_152_STRIKE_2 || of == FRAME_153_STRIKE_3 ||
of == FRAME_162_BLOCK_TO_STRIKE) {
if (roll(justblocked ? IMPBLOCKPROB[guard_skill]
: BLOCKPROB[guard_skill]))
move_3_up();
}
}
/* guard_strike (seg002:0B73): бить. По замаху Кида не бьём (кадры 169/151);
* из своего парирования по отдельной вероятности «добить». */
static void guard_strike(void)
{
uint8_t of = Opp.frame, cf = Char.frame;
if (of == FRAME_169_BEGIN_BLOCK || of == FRAME_151_STRIKE_1) return;
if (cf == FRAME_161_PARRY || cf == FRAME_150_PARRY) {
if (roll(RESTRIKEPROB[guard_skill])) move_6_shift();
} else {
if (roll(STRIKEPROB[guard_skill])) move_6_shift();
}
}
/* autocontrol_guard_kid_armed (seg002:0AC1): Кид с мечом. Вплотную и
* далеко наступаем; в «рабочей» дистанции 10..28 сначала пробуем блок, и
* только отдышавшись бьём. */
static void autocontrol_guard_kid_armed(int16_t distance)
{
if (distance < 10 || distance >= 29) {
guard_advance();
} else {
guard_block();
if (guard_refrac == 0) {
if (distance < 12 || distance >= 29) guard_advance();
else guard_strike();
}
}
}
/* autocontrol_guard_kid_in_sight (seg002:0A93). */
static void autocontrol_guard_kid_in_sight(int16_t distance)
{
if (Opp.sword == SWORD_2_DRAWN) {
autocontrol_guard_kid_armed(distance);
} else if (guard_refrac == 0) {
/* Кид безоружен: подойти и рубить. */
if (distance < 29) move_6_shift();
else move_1_forward();
}
}
/* Тайл перед стражем / через один (порт get_tile_infrontof_char и
* get_tile_infrontof2_char, seg004) но для Char, а не для Kid. */
static uint8_t tile_infront(int8_t step)
{
int8_t d = (int8_t)(Char.direction < 0 ? -step : step);
return pop_tile_at((int8_t)(Char.curr_col + d), Char.curr_row);
}
/* autocontrol_guard_kid_far (seg002:09CB): идём, если впереди есть пол хотя
* бы через тайл; иначе пятимся (не шагать в пропасть). */
static void autocontrol_guard_kid_far(void)
{
if (tile_is_floor(tile_infront(1)) || tile_is_floor(tile_infront(2)))
move_1_forward();
else
move_2_backward();
}
/* autocontrol_guard_active (seg002:737). */
static void autocontrol_guard_active(void)
{
uint8_t cf = Char.frame;
uint8_t of;
int16_t distance;
/* Кадр 166 (неактивная стойка) и всё, что ниже 150, — не боевые позы. */
if (cf == FRAME_166_STAND_INACTIVE || cf < 150 || can_guard_see_kid == 1)
return;
if (can_guard_see_kid == 0) {
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */
if (Char.charid != CHARID_4_SKELETON) move_down_back();
return;
}
/* can_guard_see_kid == 2 */
of = Opp.frame;
distance = pop_char_opp_dist();
/* Кид падает/приземляется в отскоке — не мешаем ему, ждём. */
if (distance >= 12 && of >= 102 && of < 118 && Opp.action == 5)
return;
if (distance < 35) {
if ((Char.sword < SWORD_2_DRAWN && distance < 8) || distance < 12) {
/* Слишком близко: если смотрим в одну сторону — развернуться. */
if (Char.direction == Opp.direction) move_2_backward();
else move_1_forward();
} else {
autocontrol_guard_kid_in_sight(distance);
}
return;
}
if (guard_refrac != 0) return;
if (Char.direction != Opp.direction) {
/* Кид разбежался — встречаем ударом, а не шагом. */
if (of >= 7 && of < 15) { /* бег */
if (distance < 40) move_6_shift();
return;
} else if (of >= 34 && of < 44) { /* разбег в прыжок */
if (distance < 50) move_6_shift();
return;
}
}
autocontrol_guard_kid_far();
}
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
static void autocontrol_opponent(void)
{
move_0_nothing();
/* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */
if (justblocked) --justblocked;
if (kid_sword_strike) --kid_sword_strike;
if (guard_refrac) --guard_refrac;
if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
else autocontrol_guard_active();
}
/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */
#define ACTION_99_HURT 99
#define ACTION_1_RUN 1
#define FRAME_154_POKING 154
#define SEQ_69_ATTACK_WAS_PARRIED 69
#define SEQ_74_HIT_BY_SWORD 74
#define SEQ_85_STABBED_TO_DEATH 85
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
{ 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0};
/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту.
* Возврат 1 HP кончились. Чит бессмертия ловим здесь: анимацию удара
* Кид получает, а HP не теряет. */
static uint8_t take_hp(uint8_t count)
{
if (Char.charid == CHARID_0_KID) {
if (pop_immortal) return 0;
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
hitp_delta = (int8_t)-(int8_t)count;
} else {
if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
guardhp_delta = (int8_t)-(int8_t)count;
}
return 0;
}
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
* Без меча в руке любое попадание смертельно; с мечом минус 1 HP и кадр
* «получил удар».
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
* меньше 4) не портируем ей нужны тайловые запросы ОТ Char, а pop_map
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
* идёт ровно нашей веткой. */
static void hurt_by_sword(void)
{
if (Char.alive >= 0) return;
/* Чит бессмертия перекрывает и «безоружного закалывают насмерть»:
* без этого Кид гибнет с одного удара мимо стойки, и чит бесполезен
* ровно там, где нужен. Анимацию удара оставляем. */
if (Char.charid == CHARID_0_KID && pop_immortal) {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
play_seq();
return;
}
if (Char.sword != SWORD_2_DRAWN) {
take_hp(100);
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
} else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) {
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
} else {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
}
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
play_seq(); /* TODO: звук 12/13 */
}
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
* Бьют кадры 153/154; если соперник в этот момент парирует удар отбит. */
static void check_hurting(void)
{
int16_t distance;
uint8_t cf, of, min_range;
if (Char.sword != SWORD_2_DRAWN) return;
if (Char.curr_row != Opp.curr_row) return;
cf = Char.frame;
if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return;
distance = pop_char_opp_dist();
of = Opp.frame;
if (distance < 0 || distance >= 29 ||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
/* Соперник НЕ парирует. */
if (cf == FRAME_154_POKING) {
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
distance = pop_char_opp_dist();
if (distance >= (int16_t)min_range && distance < 29)
Opp.action = ACTION_99_HURT;
}
} else {
/* Парировано. */
Opp.frame = FRAME_161_PARRY;
if (Char.charid != CHARID_0_KID) justblocked = 4;
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
play_seq();
}
}
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
void pop_check_sword_hurting(void) __banked
{
uint8_t kf = Kid.frame;
if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */
pop_loadshad_and_opp();
check_hurting();
pop_saveshad_and_opp();
pop_loadkid_and_opp();
check_hurting();
pop_savekid_and_opp();
}
/* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если
* ранен страж Киду в этом кадре урон уже не проходит (в оригинале так же:
* его action сбрасывается ДО проверки). */
void pop_check_sword_hurt(void) __banked
{
if (Guard.action == ACTION_99_HURT) {
if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN;
pop_loadshad();
hurt_by_sword();
pop_saveshad();
guard_refrac = (int8_t)REFRACTIMER[guard_skill];
} else if (Kid.action == ACTION_99_HURT) {
pop_loadkid();
hurt_by_sword();
pop_savekid();
}
}
/* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»:
* пока страж жив (alive < 0) и HP есть обычный ход; как только HP дошли до
* нуля, он переводится в «умирает» (alive = 0), и дальше тот же control()
* ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто
* доигрывает последовательность. */
static void play_guard(void)
{
if (Char.alive < 0 && guardhp_curr == 0) {
Char.alive = 0;
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
* которого нет; для уровня 1 других эффектов там нет. */
}
autocontrol_opponent();
pop_control();
}
/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у
* нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора
* последовательностей. Физика стража (fall_accel/коллизия/бамп/пики)
* отдельным шагом. */
void pop_guard_tick(void) __banked
{
if (Guard.charid == 0) return; /* стража в комнате нет */
pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */
/* Симметрично Киду (play_guard_frame, seg000:1248): cur_frame один на
* всех, и до этой строки в нём лежит кадр КИДА он тикал раньше. */
pop_load_fram_det_col();
play_guard();
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
* выбирается по Char.charid внутри load_frame (seg006:0293). */
play_seq();
pop_saveshad();
}
+38 -440
View File
@@ -8,449 +8,20 @@
#define SEQTBL_BASE 0x196Eu
/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */
/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid).
* САМА таблица кадров и seqtbl лежат в kid_data.bin (EMM-
* страница); здесь только тип и размеры. */
typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe;
#define KID_NFRAMES 241
static const kframe kid_frames[KID_NFRAMES] = {
{255, 0, 0,0x00,0x00},
{ 0, 1, 0,0xC4,0x00},
{ 1, 1, 0,0x44,0x00},
{ 2, 3, 0,0x47,0x00},
{ 3, 4, 0,0x48,0x00},
{ 4, 0, 0,0xE6,0x00},
{ 5, 0, 0,0x49,0x00},
{ 6, 0, 0,0x4A,0x00},
{ 7, 0, 0,0xC5,0x00},
{ 8, 0, 0,0x44,0x00},
{ 9, 0, 0,0x47,0x00},
{ 10, 0, 0,0x4B,0x00},
{ 11, 0, 0,0x43,0x00},
{ 12, 0, 0,0xC3,0x00},
{ 13, 0, 0,0x47,0x00},
{ 14, 0, 0,0x43,0x09},
{ 15, 0, 0,0xC3,0x00},
{ 16, 0, 0,0x44,0x00},
{ 17, 0, 0,0x46,0x00},
{ 18, 0, 0,0x48,0x00},
{ 19, 0, 0,0x89,0x00},
{ 20, 0, 0,0x0B,0x00},
{ 21, 0, 0,0x8B,0x00},
{ 22, 0, 0,0x11,0x00},
{ 23, 0, 0,0x07,0x00},
{ 24, 0, 0,0x05,0x00},
{ 25, 0, 0,0xC1,0x00},
{ 26, 0, 0,0xC6,0x00},
{ 27, 0, 0,0x43,0x00},
{ 28, 0, 0,0x48,0x00},
{ 29, 0, 0,0x42,0x00},
{ 30, 0, 0,0x42,0x00},
{ 31, 0, 0,0xC2,0x00},
{ 32, 0, 0,0xC2,0x00},
{ 33, 0, 0,0x43,0x00},
{ 34, 0, 0,0x48,0x00},
{ 35, 0, 0,0xCE,0x00},
{ 36, 0, 0,0xC1,0x00},
{ 37, 0, 0,0x45,0x00},
{ 38, 0, 0,0x8E,0x00},
{ 39, 0, 0,0x0B,0x00},
{ 40, 0, 0,0x8B,0x00},
{ 41, 0, 0,0x8A,0x00},
{ 42, 0, 0,0x01,0x00},
{ 43, 0, 0,0xC4,0x00},
{ 44, 0, 0,0xC3,0x00},
{ 45, 0, 0,0xC3,0x00},
{ 46, 0, 0,0xA5,0x00},
{ 47, 0, 0,0xA4,0x00},
{ 48, 0, 0,0x66,0x00},
{ 49, 4, 0,0x67,0x00},
{ 50, 3, 0,0x66,0x00},
{ 51, 1, 0,0x44,0x00},
{ 64, 0, 0,0xC2,0x00},
{ 65, 0, 0,0x41,0x00},
{ 66, 0, 0,0x42,0x00},
{ 67, 0, 0,0x00,0x00},
{ 68, 0, 0,0x00,0x00},
{ 69, 0, 0,0x80,0x00},
{ 70, 0, 0,0x00,0x00},
{ 71, 0, 0,0x80,0x00},
{ 72, 0, 0,0x00,0x00},
{ 73, 0, 0,0x80,0x00},
{ 74, 0, 0,0x00,0x00},
{ 75, 0, 0,0x00,0x00},
{ 76, 0, 0,0x80,0x00},
{255, 0, 0,0x00,0x00},
{ 80, -2, 0,0x41,0x00},
{ 81, -2, 0,0x41,0x00},
{ 82, -1, 0,0xC2,0x00},
{ 83, -2, 0,0x42,0x00},
{ 84, -2, 0,0x41,0x00},
{ 85, -2, 0,0x41,0x00},
{ 86, -2, 0,0x41,0x00},
{ 87, -1, 0,0x07,0x00},
{ 88, -1, 0,0x05,0x00},
{ 89, 2, 0,0x07,0x00},
{ 90, 2, 0,0x07,0x00},
{ 91, 2, -3,0x00,0x00},
{ 92, 2, -10,0x00,0x00},
{ 93, 2, -11,0x80,0x00},
{ 94, 3, -2,0x43,0x00},
{ 95, 3, 0,0xC3,0x00},
{ 96, 3, 0,0xC3,0x00},
{ 97, 3, 0,0x63,0x00},
{ 98, 4, 0,0xE3,0x00},
{ 28, 0, 0,0x00,0x00},
{ 99, 7, -14,0x80,0x00},
{100, 7, -12,0x80,0x00},
{101, 4, -12,0x00,0x00},
{102, 3, -10,0x80,0x00},
{103, 2, -10,0x80,0x00},
{104, 1, -10,0x80,0x00},
{105, 0, -11,0x00,0x00},
{106, -1, -12,0x00,0x00},
{107, -1, -14,0x00,0x00},
{108, -1, -14,0x00,0x00},
{109, -1, -15,0x80,0x00},
{110, -1, -15,0x80,0x00},
{111, 0, -15,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{112, 0, 0,0xC6,0x00},
{113, 0, 0,0x46,0x00},
{114, 0, 0,0xC5,0x00},
{115, 0, 0,0x45,0x00},
{116, 0, 0,0xC2,0x00},
{117, 0, 0,0xC4,0x00},
{118, 0, 0,0xC5,0x00},
{119, 0, 0,0x46,0x00},
{120, 0, 0,0x47,0x00},
{121, 0, 0,0x47,0x00},
{122, 0, 0,0x49,0x00},
{123, 0, 0,0xC8,0x00},
{124, 0, 0,0xC9,0x00},
{125, 0, 0,0x49,0x00},
{126, 0, 0,0x45,0x00},
{127, 2, 0,0x45,0x00},
{128, 2, 0,0xC5,0x00},
{129, 0, 0,0xC3,0x00},
{255, 0, 0,0x00,0x00},
{133, 0, 0,0x43,0x00},
{134, 0, 0,0xC4,0x00},
{135, 0, 0,0xC5,0x00},
{136, 0, 0,0x48,0x00},
{137, 0, 0,0x6C,0x00},
{138, 0, 0,0xEF,0x00},
{139, 0, 0,0x63,0x00},
{140, 0, 0,0xC3,0x00},
{141, 0, 0,0x43,0x00},
{142, 0, 0,0x43,0x00},
{143, 0, 0,0x44,0x00},
{144, 0, 0,0x44,0x00},
{172, 0, 1,0xC1,0x00},
{173, 0, 1,0xC7,0x00},
{145, 0, -12,0x01,0x00},
{146, 0, -21,0x00,0x00},
{147, 1, -26,0x80,0x00},
{148, 4, -32,0x80,0x00},
{149, 6, -36,0x81,0x00},
{150, 7, -41,0x82,0x00},
{151, 2, 17,0x42,0x00},
{152, 4, 9,0xC4,0x00},
{153, 4, 5,0xC9,0x00},
{154, 4, 4,0xC8,0x00},
{155, 5, 0,0x69,0x00},
{156, 5, 0,0xE9,0x00},
{157, 5, 0,0xE8,0x00},
{158, 5, 0,0x69,0x00},
{159, 5, 0,0x69,0x00},
{184, 0, 2,0x80,0x10},
{174, 0, 2,0x80,0x1A},
{175, 3, 2,0x00,0x12},
{176, 7, 2,0xC4,0x16},
{177, 10, 2,0x00,0x15},
{178, 7, 2,0x80,0x17},
{179, 4, 2,0x80,0x19},
{180, 0, 2,0xCE,0x18},
{181, 0, 2,0xCD,0x0F},
{182, 3, 2,0x00,0x14},
{183, 3, 2,0x00,0x1F},
{184, 0, 2,0x80,0x10},
{185, 0, 2,0x80,0x11},
{186, 0, 2,0x00,0x20},
{187, 0, 2,0x80,0x21},
{188, 2, 2,0xC3,0x22},
{ 14, 0, 0,0x43,0x00},
{189, 7, 2,0x80,0x13},
{190, 1, 2,0x80,0x0E},
{191, 0, 2,0x80,0x1B},
{181, 0, 2,0xCD,0x0F},
{181, 0, 2,0xCD,0x0F},
{112, 0, 0,0xC6,0x2B},
{113, 0, 0,0x46,0x2C},
{114, 0, 0,0xC5,0x2D},
{115, 0, 0,0x45,0x2E},
{114, 0, 0,0xC5,0x00},
{ 78, 0, 3,0x8A,0x00},
{ 77, 4, 3,0x87,0x00},
{211, 0, 1,0x44,0x00},
{212, 0, 1,0x44,0x00},
{213, 0, 1,0x44,0x00},
{214, 0, 1,0x47,0x00},
{215, 0, 7,0x4B,0x00},
{255, 0, 0,0x00,0x00},
{ 79, 4, 7,0x49,0x00},
{130, 0, 0,0x44,0x00},
{131, 0, 0,0x44,0x00},
{132, 0, 2,0x44,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{192, 0, 0,0x00,0x00},
{193, 0, 1,0x00,0x00},
{194, 0, 0,0x80,0x00},
{195, 0, 0,0x00,0x00},
{196, -1, 0,0x00,0x00},
{197, -1, 0,0x00,0x00},
{198, -1, 0,0x00,0x00},
{199, -4, 0,0x00,0x00},
{200, -4, 0,0x80,0x00},
{201, -4, 0,0x00,0x00},
{202, -4, 0,0x00,0x00},
{203, -4, 0,0x00,0x00},
{204, -4, 0,0x00,0x00},
{205, -5, 0,0x00,0x00},
{206, -5, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{207, 0, 1,0x46,0x00},
{208, 0, 1,0xC6,0x00},
{209, 0, 1,0xC8,0x00},
{210, 0, 1,0x4A,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{255, 0, 0,0x00,0x00},
{ 52, 0, 0,0x80,0x00},
{ 53, 0, 0,0x00,0x00},
{ 54, 0, 0,0x00,0x00},
{ 55, 0, 0,0x00,0x00},
{ 56, 0, 0,0x80,0x00},
{ 57, 0, 0,0x00,0x00},
{ 58, 0, 0,0x00,0x00},
{ 59, 0, 0,0x00,0x00},
{ 60, 0, 0,0x80,0x00},
{ 61, 0, 0,0x00,0x00},
{ 62, 0, 0,0x80,0x00},
{ 63, 0, 0,0x00,0x00},
{160, 1, 1,0xC3,0x23},
{161, 0, 1,0x49,0x24},
{162, 0, 1,0xC3,0x25},
{163, 0, 1,0x49,0x26},
{164, 0, 1,0xC3,0x27},
{165, 1, 1,0x49,0x28},
{166, 1, 1,0x43,0x29},
{167, 1, 1,0xC9,0x2A},
{168, 4, 1,0xC6,0x00},
{169, 3, 1,0xCA,0x00},
{170, 1, 1,0x43,0x00},
{171, 1, 1,0xC8,0x00},
};
#define KID_SEQTBL_LEN 2310
static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {
0xF9,0x01,0xFF,0x81,0x19,0xF9,0x01,0x01,0x02,0x03,0x04,0xFB,
0x08,0x05,0xFB,0x03,0x06,0xFB,0x03,0x07,0xFB,0x05,0x08,0xFB,
0x01,0xF2,0x01,0x09,0xFB,0x02,0x0A,0xFB,0x04,0x0B,0xFB,0x05,
0x0C,0xFB,0x02,0xF2,0x01,0x0D,0xFB,0x03,0x0E,0xFB,0x04,0xFF,
0x81,0x19,0xF9,0x00,0x0F,0xFF,0xA0,0x19,0xF9,0x01,0xA6,0xFF,
0xA8,0x19,0xF9,0x05,0xFB,0x0A,0xB1,0xB1,0xFB,0xF9,0xFA,0xFE,
0xB2,0xFB,0x05,0xFA,0x02,0xA6,0xFB,0xFF,0xFF,0xD2,0x19,0xFF,
0xD2,0x19,0xF9,0x01,0xFB,0x02,0xCF,0xD0,0xFB,0x02,0xD1,0xFB,
0x02,0xD2,0xFB,0x03,0xF9,0x01,0xF2,0x00,0x9E,0xAA,0xAB,0xFF,
0xD8,0x19,0xF9,0x05,0xF8,0xFF,0x00,0xAC,0xFB,0xFF,0xFA,0x01,
0xAD,0xFB,0xFF,0xAE,0xFB,0xFF,0xFA,0x02,0xFB,0xFE,0xFA,0x01,
0xFB,0xFB,0xFA,0xFC,0xFF,0x4D,0x1A,0xF9,0x01,0xF8,0x01,0x00,
0x9B,0xFB,0x02,0xA5,0xFB,0xFE,0xFF,0xD2,0x19,0xF9,0x01,0xF8,
0xFF,0x00,0x9B,0x9C,0x9D,0x9E,0xFF,0x2E,0x1A,0xF9,0x01,0xF8,
0x01,0x00,0xFB,0x02,0xA3,0xFB,0x04,0xA4,0xA5,0xFF,0xD2,0x19,
0xF9,0x01,0xF8,0x01,0x00,0xFB,0x06,0xA4,0xA5,0xFF,0xD2,0x19,
0xF9,0x01,0xF8,0xFF,0x00,0xFB,0xFD,0xA0,0xFB,0xFE,0x9D,0xFF,
0xD2,0x19,0xF9,0x01,0xF8,0xFF,0x00,0xA8,0xF9,0x01,0x97,0xF9,
0x01,0x98,0x99,0x9A,0xF9,0x05,0x9B,0xF9,0x01,0x9C,0x9D,0xFF,
0xD2,0x19,0xF9,0x01,0xA7,0xFF,0x4A,0x1A,0xA2,0xFF,0x45,0x1A,
0xA9,0x96,0xFF,0xD2,0x19,0x9F,0xA0,0xFF,0x5F,0x1A,0xF9,0x01,
0xF4,0xFF,0xD2,0x19,0xF9,0x05,0xFB,0xF8,0xFF,0xD2,0x19,0xF9,
0x05,0xA0,0x9D,0xFF,0xD2,0x19,0xF9,0x07,0xFB,0xF8,0xFF,0x39,
0x1B,0xF9,0x05,0xFE,0xFB,0x05,0xFF,0x2E,0x1A,0xF9,0x05,0xFE,
0xFB,0x12,0xFF,0xA6,0x19,0xF9,0x01,0x10,0x11,0xFB,0x02,0x12,
0xFB,0x02,0x13,0xFB,0x02,0x14,0xFB,0x02,0x15,0xFB,0x02,0x16,
0xFB,0x07,0x17,0xFB,0x09,0x18,0xFB,0x05,0xFA,0xFA,0x19,0xFB,
0x01,0xFA,0x06,0x1A,0xFB,0x04,0xF4,0xF2,0x01,0x1B,0xFB,0xFD,
0x1C,0xFB,0x05,0x1D,0xF2,0x01,0x1E,0x1F,0x20,0x21,0xFB,0x01,
0xFF,0xA0,0x19,0xF9,0x01,0xF2,0x01,0x22,0xFB,0x05,0x23,0xFB,
0x06,0x24,0xFB,0x03,0x25,0xFB,0x05,0xF2,0x01,0x26,0xFB,0x07,
0x27,0xFB,0x0C,0xFA,0xFD,0x28,0xFB,0x08,0xFA,0xF7,0x29,0xFB,
0x08,0xFA,0xFE,0x2A,0xFB,0x04,0xFA,0x0B,0x2B,0xFB,0x04,0xFA,
0x03,0x2C,0xFB,0x05,0xF4,0xF2,0x01,0xFF,0x81,0x19,0xF9,0x01,
0xFB,0x01,0x6B,0xFB,0x02,0xFB,0x02,0x6C,0xFB,0x02,0x6D,0xFB,
0x02,0x6D,0xFB,0x02,0x6D,0xFF,0x16,0x1B,0x00,0xF9,0x01,0xFB,
0x01,0x6E,0x6F,0xFB,0x02,0x70,0xFB,0x02,0x6C,0xFB,0x02,0x6D,
0xFF,0x29,0x1B,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
0xFF,0xC4,0x19,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
0xFB,0x02,0x2F,0xFB,0xFF,0x30,0xFB,0x01,0x31,0xFB,0xFE,0x32,
0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,0xFF,0xFF,0x75,0x19,
0xF9,0x01,0xFB,0x01,0x35,0xFB,0x01,0xF2,0x01,0x36,0xFB,0x08,
0x37,0xF2,0x01,0x38,0xFB,0x07,0x39,0xFB,0x03,0x3A,0xFB,0x01,
0x3B,0x3C,0xFB,0x02,0x3D,0xFB,0xFF,0x3E,0x3F,0x40,0xFB,0xFF,
0x41,0xFB,0xF2,0xFE,0xFF,0x95,0x19,0xF9,0x03,0xFA,0xFF,0x66,
0xFB,0xFE,0xFA,0x06,0x67,0xFB,0xFE,0xFA,0x09,0x68,0xFB,0xFF,
0xFA,0x0C,0x69,0xFB,0xFD,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0xF9,
0x03,0xFA,0xFF,0xFB,0xFE,0x66,0xFB,0xFD,0xFA,0x06,0x67,0xFB,
0xFD,0xFA,0x09,0x68,0xFB,0xFE,0xFA,0x0C,0x69,0xFB,0xFD,0xF8,
0x00,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0xFF,0x66,
0xFB,0x02,0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,
0x69,0xFB,0xFE,0xF8,0x01,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,
0x01,0xFA,0x03,0xF7,0x06,0x1C,0x66,0xFB,0x02,0xFA,0x06,0x67,
0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,0xFE,0xF8,0x01,
0x0F,0xFF,0x49,0x1D,0xFB,0xFF,0xFA,0xFD,0xFF,0xE4,0x1B,0xFB,
0x01,0xFF,0xDB,0x1B,0x66,0xFB,0x02,0xFA,0x03,0x67,0xFB,0xFF,
0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,0x01,0x06,0xFF,
0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,0x66,0xFB,0x02,0xFA,
0x06,0x67,0xFB,0x01,0xFA,0x09,0x68,0xFB,0x02,0xFA,0x0C,0x69,
0xF8,0x02,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,
0x66,0xFB,0x03,0xFA,0x06,0x67,0xFB,0x02,0xFA,0x09,0x68,0xFB,
0x03,0xFA,0x0C,0x69,0xF8,0x03,0x0F,0xFF,0x49,0x1D,0xF9,0x01,
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,
0x02,0x4E,0x4F,0x50,0xFF,0xA1,0x1C,0xF9,0x01,0x43,0x44,0x45,
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,0x02,0xFB,0x01,
0x4E,0xFB,0x02,0x4F,0xFB,0x01,0x50,0xFF,0xA1,0x1C,0xF9,0x01,
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFB,0xFF,
0x4D,0xF9,0x02,0xFB,0xFE,0x4E,0xFB,0xFF,0x4F,0xFB,0xFF,0x50,
0xFF,0xA1,0x1C,0xF9,0x02,0x5B,0x5A,0x59,0x58,0x57,0x57,0x57,
0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63,
0x61,0x60,0x5F,0x5E,0x5D,0x5C,0x5B,0x5A,0x59,0x58,0x57,0x58,
0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x5F,0x5E,0x5D,0x5C,
0xFF,0x25,0x1D,0xF9,0x06,0x5C,0x5D,0x5D,0x5C,0x5C,0x5B,0xFF,
0xD8,0x1C,0x87,0x88,0x89,0x89,0x8A,0x8A,0x8A,0x8A,0x89,0x88,
0x87,0xFB,0xF9,0xFF,0x25,0x1D,0xF9,0x01,0x94,0x91,0x90,0x8F,
0x8E,0x8D,0xFB,0xFB,0xFA,0x3F,0xFC,0xF9,0x03,0x8C,0x8A,0x88,
0x5B,0xF9,0x02,0xFF,0xA4,0x1C,0xF9,0x01,0x87,0x88,0x89,0x8A,
0x8B,0x8C,0xFB,0x05,0xFA,0xC1,0xFD,0x8D,0x8E,0x8F,0x90,0x91,
0x92,0x93,0x94,0xF9,0x05,0x95,0xF9,0x01,0x76,0x77,0xFB,0x01,
0xFF,0xA0,0x19,0x51,0x52,0xF9,0x05,0x53,0xF9,0x01,0xF4,0xF2,
0x00,0x54,0x55,0xFB,0x03,0xFF,0xA0,0x19,0xF9,0x03,0x51,0xFA,
0x06,0x51,0xFA,0x09,0x51,0xFA,0x0C,0xFB,0x02,0xF8,0x00,0x0C,
0xFF,0x49,0x1D,0xF9,0x04,0x6A,0xFF,0x4B,0x1D,0xF9,0x01,0x35,
0xFB,0x02,0xF2,0x01,0x36,0xFB,0x07,0x37,0xF2,0x01,0x38,0xFB,
0x02,0x31,0xFB,0xFE,0x32,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,
0xF9,0x00,0xF5,0x4F,0xFF,0x25,0x1D,0xF9,0x01,0x43,0x44,0x45,
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0xFA,0xFC,
0x4F,0xFA,0xFE,0x4F,0x4F,0xFA,0x02,0x4F,0xFA,0x04,0xFF,0x25,
0x1D,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFA,
0xFF,0x4D,0xFA,0xFD,0x4E,0xFA,0xFC,0x4F,0xFA,0xF6,0x4F,0xFA,
0xF7,0x4F,0xFA,0xF8,0x4F,0xFA,0xF9,0x4F,0xFA,0xFA,0x4F,0xFA,
0xFB,0x4F,0xFA,0xFC,0x4F,0xFA,0xFD,0x4F,0xFA,0xFE,0x4F,0xFA,
0xFE,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0x4F,
0x4F,0x4F,0xFA,0x01,0x4F,0xFA,0x01,0x4F,0xFA,0x02,0x4F,0xFA,
0x02,0x4F,0xFA,0x03,0x4F,0xFA,0x04,0x4F,0xFA,0x05,0x4F,0xFA,
0x06,0x4F,0xF8,0x00,0x06,0xFF,0x49,0x1D,0xF9,0x03,0x50,0xFF,
0xA1,0x1C,0xF9,0x05,0xFB,0xFC,0x32,0x33,0x34,0xFF,0xA0,0x19,
0xF9,0x05,0xFB,0x01,0xFA,0x03,0xF7,0x25,0x1E,0x66,0xFB,0x02,
0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,
0xFE,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0x66,0xFB,0x02,0xFA,0x03,
0x67,0xFB,0xFF,0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,
0x00,0x06,0xFF,0x49,0x1D,0xF9,0x05,0xFB,0xFF,0xFA,0xFC,0x66,
0xFB,0xFF,0xFA,0x03,0xFB,0xFD,0xFA,0x01,0xF4,0xFB,0x01,0xF2,
0x01,0x6B,0xFB,0x02,0x6C,0xF2,0x01,0x6D,0xFF,0xB3,0x1F,0x79,
0xFB,0x01,0x7A,0x7B,0xFB,0x02,0x7C,0xFB,0x04,0x7D,0xFB,0x03,
0x7E,0xFB,0xFC,0x56,0xF2,0x01,0xF4,0xFB,0xFC,0x74,0xFB,0xFE,
0x75,0x76,0x77,0xFF,0xA0,0x19,0xFB,0xFB,0xFF,0xA0,0x19,0xF9,
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x03,0x7F,0x80,0x81,
0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,
0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,
0xFB,0xFF,0xFB,0x02,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x01,0x7F,
0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,
0x03,0x7E,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFE,0x80,0x81,0x82,
0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFF,0xFC,0x1E,0xF9,
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
0x04,0x7D,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
0x7C,0xFB,0x02,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,
0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x02,
0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,
0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x01,0x81,0x82,0x83,
0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,
0x7B,0xFB,0x02,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x01,0x83,0x84,0xFF,0xA0,0x19,
0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x84,0xFF,0xA0,0x19,
0xF9,0x01,0x79,0xFB,0x01,0x84,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFF,0xAF,0x1F,0xF9,0x05,0xFB,
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0x72,0x73,0x74,
0xFB,0xFC,0x75,0x76,0x77,0xFF,0xA0,0x19,0xF9,0x01,0xF3,0x01,
0xE5,0xE5,0xE5,0xE5,0xE5,0xE5,0xE6,0xE7,0xE8,0xFF,0xDA,0x1F,
0xF9,0x01,0xFB,0xFB,0xE9,0xEA,0xEB,0xEC,0xED,0xEE,0xEF,0xF0,
0x85,0x85,0x86,0x86,0x86,0x30,0xFB,0x01,0x31,0xFB,0xFE,0xF9,
0x05,0x32,0xF9,0x01,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
0xFB,0xEA,0xEC,0xEE,0xF0,0x86,0xFB,0xFF,0xFF,0xA0,0x19,0xF9,
0x01,0xFB,0x04,0xBF,0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xF2,0x03,
0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCD,0xCD,0xF3,0x01,
0xCD,0xCD,0xC9,0xC6,0xFB,0xFC,0xFF,0xA0,0x19,0xF9,0x05,0xF4,
0xFB,0x01,0x6B,0xFB,0x02,0x6C,0xF9,0x01,0x6D,0xFF,0x36,0x20,
0xF9,0x01,0xFA,0xFE,0xFB,0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFB,
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,0x01,0x72,
0xFA,0x01,0x73,0xFB,0xFE,0xFF,0x78,0x19,0xF9,0x05,0xF4,0xFA,
0xFE,0xFB,0x01,0xFB,0x02,0x6C,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0xFB,
0x01,0x6E,0x6E,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,
0x01,0x72,0xFA,0x01,0x73,0x74,0xFB,0xFC,0x75,0x76,0x77,0xFF,
0xA0,0x19,0xF9,0x05,0xF4,0xFA,0xFE,0xFB,0x03,0xB9,0xF6,0xB9,
0xFF,0xA5,0x20,0xF9,0x05,0xFF,0xAE,0x20,0xF9,0x01,0xF6,0xB3,
0xB4,0xB5,0xB6,0xFB,0x01,0xB7,0xFB,0xFC,0xB9,0xFF,0xBA,0x20,
0xF9,0x01,0xF4,0xFB,0x04,0xB1,0xF6,0xB1,0xFF,0xC5,0x20,0xF9,
0x01,0xB2,0xF6,0xB2,0xFF,0xCD,0x20,0xFF,0x5A,0x20,0xF8,0x00,
0x00,0xF9,0x04,0xB9,0xFF,0xD9,0x20,0xF9,0x05,0xFB,0xFB,0xFA,
0xFF,0xF2,0x01,0xD9,0xDA,0xDB,0xFB,0x01,0xDC,0xFB,0xFC,0xFA,
0xFD,0xF2,0x01,0xDD,0xFB,0xFC,0xFA,0xFE,0xDE,0xFB,0xFE,0xFA,
0xFD,0xDF,0xFB,0xFD,0xFA,0xF8,0xF2,0x04,0xF2,0x01,0xE0,0xFB,
0xFF,0xFA,0xFF,0xE1,0xFB,0xFD,0xFA,0xFC,0xE2,0xFB,0xFF,0xFA,
0xFB,0xF2,0x01,0xE3,0xFB,0xFE,0xFA,0xFF,0xE4,0x00,0xF2,0x01,
0x00,0x00,0x00,0xF2,0x01,0x00,0x00,0x00,0xF2,0x01,0x00,0x00,
0x00,0xF2,0x01,0xF1,0x00,0xFF,0x2E,0x21,0x36,0xFF,0x32,0x21,
0x55,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x44,
0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x53,0x54,0x4C,0xFF,0x4B,
0x21,0xFB,0x01,0x30,0xFB,0x02,0x31,0xFB,0x06,0x32,0xFB,0x01,
0x33,0xFB,0xFF,0x34,0xFB,0x01,0x35,0xFB,0x01,0xFF,0x51,0x21,
0xFB,0x01,0x37,0x38,0xFF,0x32,0x21,0x4D,0x4E,0x4F,0x50,0x51,
0x52,0xFB,0x01,0x36,0x36,0x36,0x36,0x36,0x36,0x39,0x3A,0x3B,
0x3C,0x3D,0xFB,0x02,0x3E,0xFB,0xFF,0x3F,0xFB,0xFD,0x40,0x41,
0xFB,0xFF,0x42,0xFE,0xFB,0x10,0xFB,0x03,0xFF,0x54,0x21,0x0B,
0xFF,0x95,0x21,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFE,
0xFB,0x08,0x0B,0xFF,0x95,0x21,0xFE,0xFB,0x0B,0x0C,0xFB,0x01,
0x0D,0xFB,0x01,0x0E,0xFB,0x03,0x0F,0xFB,0x01,0x10,0x11,0xFF,
0xB8,0x21,0x13,0xFF,0xBC,0x21,0x14,0xFF,0xC0,0x21,0x15,0xFB,
0x01,0x16,0x17,0x18,0xFB,0x01,0x19,0xFB,0xFD,0x1A,0xFB,0xFE,
0x1B,0xFB,0xFC,0x1C,0xFB,0xFD,0x1D,0xFB,0xFE,0x1E,0xFB,0xFD,
0x1F,0xFB,0xFF,0x20,0x21,0xFF,0xE2,0x21,0x25,0xFF,0xE6,0x21,
0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0xFE,
0xFB,0x0C,0x0B,0xFF,0xF8,0x21,0x0B,0x0B,0xFE,0xFB,0x0D,0x2F,
0x2E,0x2D,0x2C,0x2B,0x2A,0x29,0x28,0x27,0x26,0x25,0x24,0x24,
0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,
0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,
0x22,0x22,0x23,0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,
0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,0x23,0x23,0x24,0xFF,
0x3C,0x22,0x01,0x12,0xFF,0x41,0x22,0xF9,0x01,0xBA,0xFB,0x05,
0xBA,0xFB,0x03,0xBB,0xFB,0x04,0xFF,0x47,0x22,0xBA,0xFF,0x53,
0x22,0xBC,0xFF,0x57,0x22,0xF9,0x00,0xBA,0xBA,0xBA,0xBC,0xBC,
0xBC,0xBC,0xBC,0xBC,0xBC,0xBC,0xFE,0xFB,0x08,0xFF,0x47,0x22,
0xBA,0xBA,0xBC,0xFF,0x70,0x22,
};
/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */
#define KID_BIN_FRAMES_OFF 0
#define KID_BIN_SEQTBL_OFF 1205
/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс
* frame + add_frame - 149 (add_frame=70 для кадров 102..106). */
#define KID_NGFRAMES 41
#define KID_BIN_GFRAMES_OFF 3515
#define KID_NSEQ 115
static const uint16_t kid_seq_off[KID_NSEQ] = {
@@ -471,4 +42,31 @@ static const uint16_t kid_seq_off[KID_NSEQ] = {
0x21FC,0x2240,0x2257,
};
/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.
* Индекс sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.
* Смещения x/y ЭКРАННЫЕ (применяются после calc_screen_x_coord),
* x «вперёд» по направлению персонажа. */
#define KID_NSWORD 51
#define KID_SWORD_ID_INIT { \
0xFF,0x00,0x05,0x01,0x02,0x06,0x07,0x03,0x04,0x1E,0x08,0x09, \
0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x13,0x11, \
0x11,0x07,0x01,0x1C,0x08,0x04,0x04,0x0A,0x0D,0x0D,0x0D,0x14, \
0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0xFF,0xFF,0x1D,0x08,0x1F, \
0x20,0x21,0x08, \
}
#define KID_SWORD_DX_INIT { \
0,0,-9,7,17,7,0,17,16,12,13,7, \
10,10,22,28,13,0,0,21,14,21,22,7, \
15,0,7,14,7,6,9,11,24,19,21,7, \
14,14,14,28,28,21,14,14,21,0,8,14, \
14,7,8, \
}
#define KID_SWORD_DY_INIT { \
0,-9,-29,-25,-26,-14,-5,-16,-19,-9,-34,-25, \
-16,-11,-21,-23,-35,-38,-29,-19,-23,-22,-23,-13, \
-18,-8,-27,-28,-27,-23,-21,-18,-23,-23,-23,-32, \
-32,-31,-29,-28,-28,-25,-22,-25,-25,-16,-37,-24, \
-24,-14,-37, \
}
#endif
File diff suppressed because it is too large Load Diff
+134 -4
View File
@@ -13,13 +13,30 @@
#include <stdint.h>
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
* POP_YOFF = (256-200)/2 = 28px (чёрные борта сверху/снизу). Внутри 200px
* верхние POP_PLAYFIELD_H = 192 поле тайлов/спрайтов (комната = 3 ряда),
* нижние 8 статус-полоса (жизни/сообщения): тайлы/спрайты туда НЕ лезут.
* Сдвиг применяется на границе примитивов (blit/heal) логика рисования
* остаётся в «room-space» y=0..191. */
#define POP_YOFF 28
#define POP_PLAYFIELD_H 192
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
/* Выбор ядра libbgi (pop_onscreen_cols / pop_heal_fast) переехал в
* `_pop_draw.h` его включают только три рисующих модуля. Из общего
* заголовка SDCC размножал мёртвые копии этих inline'ов по всем TU: см.
* шапку `_pop_draw.h`. */
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
* после initgraph (gfx_pal_fload). 0 OK, -1 ошибка. */
int pop_bg_load(void);
int pop_bg_load(void) __banked;
/* Освободить EMM-страницы атласов. */
void pop_bg_free(void);
void pop_bg_free(void) __banked;
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
* (по строкам, row*10+col). lcol_fg/lcol_bg 3 тайла правого столбца
@@ -27,13 +44,126 @@ void pop_bg_free(void);
* тогда левая кромка = стена. room_num номер комнаты (зерно стен). */
void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg,
const uint8_t *lcol_fg, const uint8_t *lcol_bg);
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked;
/* Задать ряд 0 комнаты СНИЗУ — для draw_tile_topright, когда тайл нижнего
* ряда стал empty (после падения loose): «стена вниз» рисуется по коду
* room_B(0, col-1). ОДИННАДЦАТЬ байт: [0..9] колонки комнаты снизу,
* [10] тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
* load_rowbelow, seg008:368). 0 нет комнаты снизу. */
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 нет
* комнаты сверху (кромка уровня пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
* звать ПОСЛЕ kid_draw. obj_x ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y низ
* спрайта, w/h размер кадра, dir направление Kid (<0 влево). Метрики
* берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir, uint8_t frame, uint8_t action);
int8_t dir, uint8_t frame, uint8_t action) __banked;
/* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с
* POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит
* тот, кто нарисовал спрайт (kid_draw / pop_guard_draw) он один знает
* итоговый габарит (у Kid с учётом клинка и обрезки clip_char). Вне
* fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и
* замер в pop_bg.c у fclip_on. */
void pop_fore_set_clip(int x, int y, int w, int h) __banked;
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
* без оверлеев поз виса/полёта/подъёма. obj_x ЛОГИЧЕСКАЯ координата
* (до ×8/7), как у kid_fp_obj_x(). */
/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому
* вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в
* кадр, других вызывающих нет цена одного трамплина здесь не видна. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir, uint8_t sword) __banked;
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
* прямоугольник Kid, поэтому kid_heal его не убирает на второй странице
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
void pop_fore_heal(void) __banked;
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
* shake_draw дрожащий кадр (heal+SPRITE); bake_empty запечь пустоту
* после падения (NORMAL, в ОЗУ-копию). */
void pop_loose_shake_draw(int row, int col) __banked;
void pop_loose_bake_empty(int row, int col) __banked;
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
* Состояние pop_ceil_modif[10] в pop_map. */
void pop_ceil_shake_draw(int col) __banked;
void pop_ceil_bake_empty(int col) __banked;
/* Кусок приземлился (loose_land) — переехал в pop_state.h: переменную
* читают по обе стороны банковой границы. */
#include "pop_state.h"
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
void pop_spike_redraw(int row, int col) __banked;
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
* modif живой room_modif тайла. Для факела col колонка САМОГО факела
* (пламя рисуется в ячейке правого соседа). */
void pop_torch_draw(int row, int col, uint8_t modif) __banked;
void pop_potion_draw(int row, int col, uint8_t modif) __banked;
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
void pop_room_redraw_seam_left(void) __banked;
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
* кнопка нажата/отжата, floor debris от упавшей плиты. */
void pop_floor_bake(int row, int col) __banked;
void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кнопка) */
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
* запечкой створка анимируется (pop_trob animate_leveldoor). */
void pop_leveldoor_redraw(int row, int col) __banked;
/* Ворота ВНУТРИ отрисованной комнаты: перерисовать по живому modif.
* (row,col) координаты САМИХ ворот; бары рисует ячейка правого соседа
* (draw_tile, ветка lcode==4), туда и идёт wipe+redraw. Порт draw_trob
* (seg007:01E6), которым заканчивается animate_door. */
void pop_gate_redraw(int row, int col) __banked;
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col) __banked;
void pop_loose_mob_tick(void) __banked;
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
/* Смена комнаты: куски продолжают падать в СВОИХ комнатах (do_mobs крутит
* mobs[] независимо от drawn_room), сбрасывается только heal-история. */
void pop_loose_mob_room_changed(void) __banked;
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после kid_draw, перед fore-слоем. */
void pop_loose_mob_draw_over(void) __banked;
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
uint8_t pop_loose_mob_pos(int *col, int *y) __banked;
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
* верхнего борта взводит внутренний флаг «борта грязные» на 2 кадра.
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked;
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
void pop_room_clip_borders(void) __banked;
#endif
+32
View File
@@ -0,0 +1,32 @@
/*
* pop_char.h состояние ПЕРСОНАЖА, общее для Кида, стража и активной
* копии Char. Порт char_type (types.h:302).
*
* Зачем отдельным заголовком: интерпретатор последовательностей в
* оригинале один на всех (работает с Char, а вокруг стоят
* loadkid/savekid и loadshad/saveshad, seg006:809). Пока у Кида был свой
* усечённый тип, а у стража свой, общий интерпретатор был невозможен.
* Поля и их порядок как в char_type, чтобы порт seg005/seg006 ложился
* один в один.
*/
#ifndef POP_CHAR_H
#define POP_CHAR_H
#include <stdint.h>
typedef struct {
uint8_t frame;
uint8_t x, y;
int8_t direction; /* -1 влево, 0 вправо */
int8_t curr_col, curr_row;
uint8_t action;
int8_t fall_x, fall_y;
uint8_t room;
uint8_t repeat;
uint8_t charid; /* 0 = Kid, 2 = страж (см. CHARID_* в pop_guard.h) */
uint8_t sword;
int8_t alive; /* -1 = жив (как в оригинале), 0 = убит */
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
} pop_char_t;
#endif
+30
View File
@@ -0,0 +1,30 @@
/*
* pop_cheat.h режим читов (порт cheats_enabled, seg000:111).
*
* В оригинале включается ключом командной строки megahit/debug и меняет
* набор доступных клавиш. У нас конфигурации/UI пока нет, поэтому это
* просто глобальный флаг: main ставит его при старте (на время разработки
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
*
* Реализовано: K убить стража (seg000:786), I/S/Shift+L (наши отладочные),
* [ / ] подгонка Кида по X (seg000:1828). Остальные читы оригинала
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
*/
#ifndef POP_CHEAT_H
#define POP_CHEAT_H
#include <stdint.h>
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
/* Последний уровень игры (tbl_* в pop_level рассчитаны на 0..15). */
#define POP_LAST_LEVEL 15
#endif
+348 -31
View File
@@ -17,8 +17,16 @@
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h" /* charid, состояние меча, seq стража */
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
* scancodes. TODO: убрать после отладки. */
#define KBD_DBG_STEPL 0x3B /* J */
#define KBD_DBG_STEPR 0x4B /* L */
/* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */
#define CONTROL_RELEASED 0
@@ -45,12 +53,15 @@
#define SEQ_49_STAND_UP_FROM_CROUCH 49
#define SEQ_50_CROUCH 50
#define SEQ_79_CROUCH_HOP 79
#define SEQ_78_DRINK 78 /* выпить зелье */
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
#define SEQ_44_STEP_ON_EDGE 44
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */
#define SEQ_25_HANG_AGAINST_WALL 25 /* вис у стены (Shift в висе) */
#define SEQ_GO_UP_LEVELDOOR 70 /* seq_70_go_up_on_level_door */
#define FRAME_7_RUN 7
#define FRAME_11_RUN 11
@@ -63,18 +74,25 @@
#define FRAME_109_CROUCH 109
/* ---- Состояние управления (seg006 globals) ------------------------ *
* control_x/y/shift «сырое» состояние осей ЭТОГО кадра (абсолютно:
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 конечный
* автомат held/released/ignore (авто-повтор); ctrl1_* их память между
* кадрами (rest/save). */
static int8_t control_x, control_y, control_shift;
static int8_t control_forward, control_backward, control_up, control_down, control_shift2;
* Сами control_* ЖИВУТ В pop_state.c, а не здесь. Модуль уехал в БАНК 5,
* а шину пишут с трёх сторон (этот банк, guards.c из банка 1) и читают из
* банка 3 (pop_map) это ровно тот случай, для которого pop_state и
* заведён. Сегодня оно работало бы и отсюда: банки собираются БЕЗ
* --bank-data, поэтому их писучие данные остаются в общем _DATA. Но это
* флаг сборки, а не свойство кода: включат --bank-data ради экономии
* W1/W2 и шина молча уедет в страницу банка.
*
* ctrl1_* остаются здесь: их трогает только этот модуль. */
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
* Обёртка над kid_set_seq (единая точка в pop_kid kid_seq_off/kid_seqtbl
* не дублируются в этот TU). */
#define seqtbl_offset_char(seq) kid_set_seq(seq)
/* В оригинале seqtbl_offset_char работает с АКТИВНЫМ персонажем, и
* control() вызывается внутри окна Char. Раньше макрос вёл на
* kid_set_seq (пишет прямо в Kid) после введения окна это ломало ввод:
* savekid затирал Kid копией Char со старой curr_seq. */
#define seqtbl_offset_char(seq) pop_char_set_seq(seq)
static int8_t release_arrows(void)
{
@@ -98,17 +116,43 @@ static void standing_jump(void)
seqtbl_offset_char(SEQ_3_STANDING_JUMP);
}
/* check_get_item (seg005:061F): решение по тайлам принимает pop_map, здесь
* только исполнение. Триггер в оригинале Shift (control_shift2) в стойке
* или в приседе: первый раз Kid приседает к предмету, второй поднимает.
* Возврат 1 = управление обработано (caller делает return). */
static uint8_t get_item_action(void)
{
uint8_t a = pop_get_item_action();
if (a == 1) { crouch(); return 1; }
if (a == 2) {
control_shift2 = CONTROL_IGNORE; /* do_pickup: без авто-повтора */
seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
return 1;
}
if (a == 3) { /* зелье — выпить (get_item) */
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_78_DRINK);
return 1;
}
return 0;
}
/* run_jump (seg005:0AA8). Выравнивание по кромке пола живёт в pop_map
* (там тайловые запросы) pop_run_jump_align; здесь только диспетчерская
* часть. Отказ выравнивателя = прыжка в этом кадре НЕТ, и control_up
* гасить нельзя: Кид бежит дальше с зажатой «вверх» и попробует снова на
* следующем кадре. Ровно так игрок и «ловит» фазу перед провалом. */
static void run_jump(void)
{
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
* полировка K3; K2b просто запускает run-jump. */
if (Kid.frame >= FRAME_7_RUN) {
if (Char.frame >= FRAME_7_RUN) {
if (!pop_run_jump_align()) return;
control_up = release_arrows();
seqtbl_offset_char(SEQ_4_RUN_JUMP);
}
}
static void safe_step(void); /* fwd */
static void draw_sword(void); /* fwd: нужен в control_standing */
static void forward_pressed(void)
{
@@ -149,8 +193,15 @@ static void jump_up(void)
static void up_pressed(void)
{
/* верь уровня/телепорт — нет в тесте.) ↑+forward = прыжок с места;
* иначе вертикальный прыжок вверх. */
/* Дверь уровня рядом и открыта → войти в неё: постановку в проём
* (go_up_leveldoor) делает pop_leveldoor_enter, здесь остаётся только
* последовательность подъёма по лестнице. Телепортов у нас нет это
* модификация SDLPoP, а не оригинал.
* Иначе: +forward = прыжок с места, иначе вертикальный прыжок вверх. */
if (pop_leveldoor_enter()) {
seqtbl_offset_char(SEQ_GO_UP_LEVELDOOR);
return;
}
if (control_x == CONTROL_HELD_FORWARD)
standing_jump();
else
@@ -167,15 +218,15 @@ static void safe_step(void)
control_forward = CONTROL_IGNORE;
distance = pop_edge_distance();
if (distance) {
Kid.repeat = 1;
Char.repeat = 1;
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
} else if (pop_edge_type() == EDGE_WALL) {
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
Kid.repeat = 1;
Char.repeat = 1;
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
} else if (Kid.repeat != 0) {
Kid.repeat = 0;
} else if (Char.repeat != 0) {
Char.repeat = 0;
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
} else {
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
@@ -185,6 +236,8 @@ static void safe_step(void)
/* ---- control_* обработчики состояний ------------------------------ */
static void control_crouched(void)
{
/* Shift в приседе — поднять предмет (seg005 control_crouched). */
if (control_shift2 == CONTROL_HELD && get_item_action()) return;
if (control_y != CONTROL_HELD_DOWN) {
seqtbl_offset_char(SEQ_49_STAND_UP_FROM_CROUCH);
} else if (control_forward == CONTROL_HELD) {
@@ -195,7 +248,37 @@ static void control_crouched(void)
static void control_standing(void)
{
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */
/* Shift в стойке над предметом — присесть к нему (seg005:0358, первая
* строка control_standing). Идёт ДО остальных shift-веток. */
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
get_item_action())
return;
/* Ветку «charid != kid && вниз+вперёд -> draw_sword» не портируем: у нас
* соперник до control_standing не доходит, его перехватывает
* control_guard_inactive в самом диспетчере (порядок как в seg005). */
/* seg005:352 — Кид с мечом ВИДИТ активного стража: достаёт клинок сам.
* Без этого игрок стоит с мечом в ножнах, пока его рубят. */
if (pop_have_sword) {
if (offguard != 0 && control_shift >= CONTROL_RELEASED) {
/* Меч убран намеренно (down в бою) — не доставать, пока не
* отпустят Shift; в оригинале это переход на loc_6213. */
} else if (can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d >= -10 && d < 90) {
holding_sword = 1;
/* (word)d < (word)-6 — беззнаковое: истинно для ВСЕГО, кроме
* 6..1. То есть «страж вплотную за спиной» -> отступить,
* иначе достать меч. */
if (d >= -6 && d < 0) { back_pressed(); return; }
draw_sword();
return;
}
} else {
offguard = 0;
}
}
if (control_shift == CONTROL_HELD) {
if (control_backward == CONTROL_HELD) back_pressed();
else if (control_up == CONTROL_HELD) up_pressed();
@@ -236,7 +319,7 @@ static void control_running(void)
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
* seq_47 при беге в стену (как оригинал), с корректной позицией. */
if (control_x == CONTROL_RELEASED &&
(Kid.frame == FRAME_7_RUN || Kid.frame == FRAME_11_RUN)) {
(Char.frame == FRAME_7_RUN || Char.frame == FRAME_11_RUN)) {
control_forward = release_arrows();
seqtbl_offset_char(SEQ_13_STOP_RUN);
} else if (control_x == CONTROL_HELD_BACKWARD) {
@@ -287,11 +370,216 @@ static void control_jumpup(void)
standing_jump();
}
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */
static void control(void)
/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
* клинка со звуком), соперник сразу в стойку en garde (seq_90). Стрелки
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
* уже как выпад. */
static void draw_sword(void)
{
uint8_t f = Kid.frame;
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
uint8_t seq_id = SEQ_55_DRAW_SWORD;
control_forward = control_shift2 = release_arrows();
if (Char.charid == CHARID_0_KID) {
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
} else if (Char.charid != CHARID_1_SHADOW) {
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
}
Char.sword = SWORD_2_DRAWN;
seqtbl_offset_char(seq_id);
}
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд достать меч,
* вниз без «вперёд» развернуться (stand flipped). */
static void control_guard_inactive(void)
{
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
if (control_forward == CONTROL_HELD) {
draw_sword();
} else {
control_down = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
}
}
}
/* ---- Боёвка (seg005:964..) ---------------------------------------- *
* Всё это ветки ОБЩЕГО диспетчера: у стража тот же control(), просто
* control_* ему выставляет ИИ (autocontrol_*, guards.c), а не клавиатура. */
/* back_with_sword (seg005): шаг назад — только из стоек. */
static void back_with_sword(void)
{
uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_backward = CONTROL_IGNORE; /* без авто-повтора */
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD);
}
}
/* forward_with_sword (seg005): шаг вперёд; у стража своя последовательность. */
static void forward_with_sword(void)
{
uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_forward = CONTROL_IGNORE;
seqtbl_offset_char(Char.charid != CHARID_0_KID ? SEQ_56_GUARD_FORWARD
: SEQ_86_FORWARD_WITH_SWORD);
}
}
/* sword_strike (seg005): выпад из стойки/шага либо добивание после парирования. */
static void sword_strike(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_157_WALK_WITH_SWORD || f == FRAME_158_STAND_WITH_SWORD ||
f == FRAME_170_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD ||
f == FRAME_165_WALK_WITH_SWORD) {
seq_id = (Char.charid == CHARID_0_KID) ? SEQ_75_STRIKE : SEQ_58_GUARD_STRIKE;
} else if (f == FRAME_150_PARRY || f == FRAME_161_PARRY) {
seq_id = SEQ_66_STRIKE_AFTER_PARRY;
} else {
return;
}
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
}
/* parry (seg005): блок. Условие сильно разное у Кида и у соперника —
* соперник блокирует ТОЛЬКО на кадре 152 (второй кадр удара), а Кид ещё и
* по 153 (и тогда последовательность прокручивается сразу). */
static void parry(void)
{
uint8_t cf = Char.frame, of = Opp.frame;
uint8_t seq_id = SEQ_62_PARRY;
uint8_t do_play_seq = 0;
if (cf == FRAME_158_STAND_WITH_SWORD || cf == FRAME_170_STAND_WITH_SWORD ||
cf == FRAME_171_STAND_WITH_SWORD || cf == FRAME_168_BACK ||
cf == FRAME_165_WALK_WITH_SWORD) {
if (pop_char_opp_dist() >= 32 && Char.charid != CHARID_0_KID) {
back_with_sword();
return;
} else if (Char.charid == CHARID_0_KID) {
if (of == FRAME_168_BACK) return;
if (of != FRAME_151_STRIKE_1 && of != FRAME_152_STRIKE_2 &&
of != FRAME_162_BLOCK_TO_STRIKE) {
if (of == FRAME_153_STRIKE_3) do_play_seq = 1;
}
} else {
if (of != FRAME_152_STRIKE_2) return;
}
} else {
if (cf != FRAME_167_BLOCKED) return;
seq_id = SEQ_61_PARRY_AFTER_STRIKE;
}
control_up = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
if (do_play_seq) play_seq();
}
/* swordfight (seg005:0CDB) — разбор ввода в бою. Порядок веток важен:
* удар старше «убрать меч», тот старше блока, блок старше шагов. */
static void swordfight(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_161_PARRY && control_shift2 >= CONTROL_RELEASED) {
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD); /* отскок после парирования */
return;
} else if (control_shift2 == CONTROL_HELD) {
if (Char.charid == CHARID_0_KID) kid_sword_strike = 15;
sword_strike();
if (control_shift2 == CONTROL_IGNORE) return;
}
if (control_down == CONTROL_HELD) {
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_down = CONTROL_IGNORE;
Char.sword = SWORD_0_SHEATHED;
if (Char.charid == CHARID_0_KID) {
offguard = 1; /* убрал намеренно — сразу не доставать */
guard_refrac = 9; /* и страж на это время придержан */
holding_sword = 0;
seq_id = SEQ_93_PUT_SWORD_AWAY_FAST;
} else if (Char.charid == CHARID_1_SHADOW) {
seq_id = SEQ_92_PUT_SWORD_AWAY;
} else {
seq_id = SEQ_87_GUARD_BECOME_INACT; /* Кид ушёл из виду */
}
seqtbl_offset_char(seq_id);
}
} else if (control_up == CONTROL_HELD) {
parry();
} else if (control_forward == CONTROL_HELD) {
forward_with_sword();
} else if (control_backward == CONTROL_HELD) {
back_with_sword();
}
}
/* control_with_sword (seg005:964): пока соперник виден и близко — бой;
* иначе страж убирает меч, а Кид просто стоит. */
static void control_with_sword(void)
{
if (Char.action >= 2) return; /* actions_2_hang_climb и выше */
if (pop_tile_at(Char.curr_col, Char.curr_row) == 11 /* loose */ ||
can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d < 90) {
if (d >= 0) { swordfight(); return; }
/* Соперник за спиной: далеко — развернуться, вплотную — драться. */
if (d < -4) { seqtbl_offset_char(SEQ_60_TURN_WITH_SWORD); return; }
swordfight();
return;
}
}
/* Не видит / далеко. */
if (Char.charid < CHARID_2_GUARD) {
if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
Char.sword = SWORD_0_SHEATHED;
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); /* страж убит */
}
} else {
swordfight();
}
}
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
void pop_control(void) __banked
{
uint8_t f = Char.frame;
/* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0).
* Из любой стойки он валится в seq_71_dying; остальное управление в
* этом состоянии не обрабатывается. Через это же место проходит чит K
* (pop_guard_kill ставит Guard.alive = 0). */
if (Char.alive >= 0) {
if (f == FRAME_15_STAND || f == FRAME_166_STAND_INACTIVE ||
f == FRAME_158_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD)
seqtbl_offset_char(SEQ_71_DYING);
return;
}
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
* НЕ обрабатывается вовсе только гасятся стрелки. Без этого игрок
* прерывает присед после удара (medland/плита по голове) нажатием , и
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
* стартовый dy(-2) Kid навсегда остаётся на 2px выше пола (баг «Y на
* пару пикселей меньше, чем должен»). */
if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
release_arrows();
return;
}
/* Порядок веток — как в seg005: меч старше charid, charid старше кадров.
* Пока Kid безоружен, обе ветки про стража. */
if (Char.sword == SWORD_2_DRAWN)
control_with_sword();
else if (Char.charid >= CHARID_2_GUARD)
control_guard_inactive();
else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
control_standing();
else if (f == FRAME_48_TURN)
control_turning();
@@ -314,9 +602,14 @@ static void read_input(void)
uint8_t r = kbd_raw_down(KBD_RIGHT);
uint8_t u = kbd_raw_down(KBD_UP);
uint8_t d = kbd_raw_down(KBD_DOWN);
/* Отладочный осторожный шаг: J/L = Shift+влево/вправо (см. KBD_DBG_STEP*). */
uint8_t sl = kbd_raw_down(KBD_DBG_STEPL);
uint8_t sr = kbd_raw_down(KBD_DBG_STEPR);
if (sl) l = 1;
if (sr) r = 1;
control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED);
control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED);
control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
control_shift = (sl || sr || kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
? CONTROL_HELD : CONTROL_RELEASED;
}
@@ -361,12 +654,12 @@ static void flip_control_x(void)
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
static void user_control(void)
{
if (Kid.direction >= DIR_RIGHT) {
if (Char.direction >= DIR_RIGHT) {
flip_control_x();
control();
pop_control();
flip_control_x();
} else {
control();
pop_control();
}
}
@@ -382,7 +675,7 @@ static void save_ctrl_1(void)
}
/* ---- Публичный API ------------------------------------------------ */
int pop_ctrl_init(void)
int pop_ctrl_init(void) __banked
{
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
control_shift2 = CONTROL_RELEASED;
@@ -390,22 +683,46 @@ int pop_ctrl_init(void)
return kbd_raw_open();
}
void pop_ctrl_close(void)
void pop_ctrl_close(void) __banked
{
kbd_raw_close();
}
int8_t pop_ctrl_shift_held(void)
int8_t pop_ctrl_shift_held(void) __banked
{
return (int8_t)(control_shift == CONTROL_HELD);
}
void pop_ctrl_tick(void)
void pop_ctrl_tick(void) __banked
{
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */
/* control_kid (seg006:0CD1), ПЕРВАЯ строка: HP кончились — персонаж
* мёртв. Без этой ветки смерть от меча стража не доводилась до конца:
* hurt_by_sword играл seq_85, hitp_curr падал в 0, но Kid.alive
* оставался 1 и pop_kid_dead не взводился Кид досматривал сиквенс,
* возвращался в стойку с нулём HP, и страж убивал его снова по кругу
* (BUG-DEATH-1). pop_kid_dead наш сигнал главному циклу на респавн
* (в оригинале это счётчик Char.alive в play_kid, seg006:0BEE). */
if (Kid.alive < 0 && hitp_curr == 0) {
Kid.alive = 0;
pop_kid_dead = 1;
}
rest_ctrl_1();
read_input();
read_user_control();
user_control(); /* -> control(): может сменить Kid.curr_seq */
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
* персонажем (как в оригинале), поэтому ввод игрока применяется к
* Киду только внутри loadkid/savekid.
*
* load_fram_det_col ОБЯЗАТЕЛЕН здесь порт play_kid_frame
* (seg000:1209), где он стоит ровно между loadkid_and_opp() и
* play_kid(). cur_frame один на всех персонажей, и в кадре последним
* его перезаписывает страж; без перезагрузки диспетчер считает
* геометрию Кида (dx_weight -> determine_col -> distance_to_edge_weight
* -> get_edge_distance) по кадру СТРАЖА. Замерено: BUG-CTRL-FRAME-1. */
pop_loadkid();
pop_load_fram_det_col();
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
pop_savekid_state();
save_ctrl_1();
}
+37 -4
View File
@@ -18,18 +18,51 @@
#include <stdint.h>
/* МОДУЛЬ ЖИВЁТ В БАНКЕ 5 (--bank 5=pop_ctrl.c), поэтому всё, что зовут
* снаружи, помечено __banked. Зовут с трёх сторон: главный цикл
* (резидент), guards.c (банк 1) и pop_map.c (банк 3) банкбанк
* работает, трамплин сохраняет страницу окна на стеке. Диспетчер
* дёргается один раз за кадр на персонажа, так что цена перехода
* незаметна. Сама шина control_* лежит в pop_state.c (см. ниже). */
/* Открыть raw-канал клавиатуры и обнулить память авто-повтора.
* 0 OK, -1 ошибка (EBUSY и пр., см. kbd_raw_open). */
int pop_ctrl_init(void);
void pop_ctrl_close(void);
int pop_ctrl_init(void) __banked;
void pop_ctrl_close(void) __banked;
/* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать
* диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД
* kid_tick() (play_seq продолжит с новой последовательности). */
void pop_ctrl_tick(void);
void pop_ctrl_tick(void) __banked;
/* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map):
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
int8_t pop_ctrl_shift_held(void);
int8_t pop_ctrl_shift_held(void) __banked;
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
* диспетчера: он ВЫСТАВЛЯЕТ эти переменные, а дальше исполняется тот же
* control(), что и для игрока. Значения CONTROL_RELEASED/IGNORE/HELD. */
#define POP_CONTROL_RELEASED 0
#define POP_CONTROL_IGNORE 1
#define POP_CONTROL_HELD (-1)
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
* заполняет как есть, без flip_control_x. */
/* Определены в pop_state.c (резидент W1/W2), а не в pop_ctrl.c: шину
* трогают три банка сразу, а данные банкового модуля живут в _DATA лишь
* пока сборка идёт без --bank-data. */
extern int8_t control_x, control_y, control_shift;
#define POP_CONTROL_HELD_FORWARD (-1)
#define POP_CONTROL_HELD_BACKWARD 1
#define POP_CONTROL_HELD_UP (-1)
#define POP_CONTROL_HELD_DOWN 1
extern int8_t control_forward, control_backward, control_up, control_down,
control_shift2;
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж из
* своего тика напрямую. */
void pop_control(void) __banked;
#endif
+29
View File
@@ -0,0 +1,29 @@
/*
* pop_draw.c выбор ядра libbgi для heal: линейное «noclip», если
* прямоугольник целиком на экране, иначе общее клипающее. Объявление и
* мотивация `_pop_draw.h`.
*
* ПОЧЕМУ ОТДЕЛЬНЫЙ РЕЗИДЕНТНЫЙ МОДУЛЬ, а не inline в заголовке. Замер
* CLIP-1 (2026-08-01), SDCC 4.5: `static inline` тут обходится втридорога
* компилятор И встраивает тело в каждый вызов (181 Б на место), И оставляет
* отдельную мёртвую копию в каждом TU. У стража это раздуло
* `pop_guard_heal` с ~60 до 663 Б. Пяти местам вызова это стоило бы ~1.4 КБ
* ради экономии ~50 тактов на вызов при том что BANK2 занят на 86 %.
*
* Резидент W1 (_CODE), а не банк: сюда зовут из ДВУХ разных банков
* (pop_bg.c BANK2, pop_gdraw.c BANK4) и из резидента (pop_kid.c). W1
* замаплено всегда, поэтому из банка это обычный `call` без трамплина
* тот же приём, что у pop_sword_draw.
*/
#include <stdint.h>
#include <gfx.h>
#include "_pop_draw.h"
void pop_heal_fast(int x, int y, int w, int h)
{
if (w <= 0 || h <= 0) return;
if (pop_onscreen_cols(x, y, (uint16_t)w, (uint16_t)h))
gfx_heal_noclip(x, y, (uint8_t)w, (uint8_t)h);
else
gfx_heal(x, y, w, h);
}
+237
View File
@@ -0,0 +1,237 @@
/*
* pop_gdraw.c ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c).
*
* Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ
* целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны
* остаться в W1/W2 из банка (guards.c) резидент W3 недостижим ни прямо,
* ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит
* ровно между «что рисует» (зовёт только главный цикл резидент) и «что
* знает» (зовёт и банк W1/W2).
*
* Писучие статики резидента (--dataseg не трогаем) остаются в обычном
* _DATA, то есть в W2 сюда переезжает КОД, не данные.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_guard.h"
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
#include "kid_data.h"
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
#define GUARD_PAGES 5
static atlas_t gp[GUARD_PAGES];
static uint8_t g_ok;
/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid:
* heal обязан стирать спрайт именно той страницы, в которую рисуем). */
static int gx_l[2], gy_l[2];
static uint16_t gw_l[2], gh_l[2];
static uint8_t gvalid[2];
/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение
* почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит
* ровно по площади. Объединение считаем только для окна fore-клипа. */
static int sx_l[2], sy_l[2];
static uint16_t sw_l[2], sh_l[2];
static uint8_t svalid[2];
/* Ширина спрайта последнего кадра стража — нужна set_char_collision
* (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам:
* это свойство кадра, а не буфера. Отставание на кадр допустимо так же
* работает kid_fp_width() у Кида. */
static uint16_t g_last_w;
uint16_t pop_guard_fp_width(void) __banked { return g_last_w; }
int pop_guard_load(void) __banked
{
static const char *const names[GUARD_PAGES] = {
"GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
"GUARD\\g3.atl", "GUARD\\g4.atl"
};
uint8_t i;
for (i = 0; i < GUARD_PAGES; i++)
if (atlas_load(&gp[i], names[i]) != 0) return -1;
g_ok = 1;
return 0;
}
void pop_guard_free(void) __banked
{
uint8_t i;
if (!g_ok) return;
for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]);
g_ok = 0;
}
/* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же
* причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в
* большинстве кадров noclip-ядром. Два прямоугольника, а не объединение
* (см. комментарий к sx_l выше). */
void pop_guard_heal(void) __banked
{
uint8_t p = gfx_get_draw_page() & 1;
if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]);
gvalid[p] = svalid[p] = 0;
}
void pop_guard_draw(void) __banked
{
int obj_x, obj_y, top, fwd, fp_x;
uint8_t page, idx, p, flip;
const uint8_t *img;
uint16_t w, h;
/* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам
* футпринт (pop_fore_over_char) считается по спрайту стража, поэтому
* w/h ниже НЕ трогаем. */
int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0;
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx);
obj_x = (fwd << 1) - 116;
obj_y = (int)Guard.y + pop_gframe.dy;
if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++;
fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */
obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */
page = (uint8_t)(pop_gframe.image >> 3);
idx = (uint8_t)(pop_gframe.image & 7);
if (page >= GUARD_PAGES) return;
img = (const uint8_t *)atlas_image(&gp[page], idx);
gfx_w0_map(gp[page].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
int bx = obj_x;
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
if (flip) bx -= (int)w;
top = obj_y - (int)h + 1;
p = gfx_get_draw_page() & 1;
gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
else
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
g_last_w = w; /* для char_width_half, см. выше */
clip_x = bx; clip_y = top; clip_w = w; clip_h = h;
}
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
* heal; окно fore-клипа объединение (без него клинок полез бы поверх
* переднего столба). */
{
int sx, sy; uint16_t sw, sh;
if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y,
&sx, &sy, &sw, &sh)) {
p = gfx_get_draw_page() & 1;
sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh;
svalid[p] = 1;
if (!clip_w) {
clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh;
} else {
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
if (sx < clip_x) clip_x = sx;
if (sy < clip_y) clip_y = sy;
if (sx + (int)sw > x1) x1 = sx + (int)sw;
if (sy + (int)sh > y1) y1 = sy + (int)sh;
clip_w = (uint16_t)(x1 - clip_x);
clip_h = (uint16_t)(y1 - clip_y);
}
}
}
gfx_w0_unmap();
/* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
* fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы 432 К тактов. */
pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
* midtable, а foretable рисуется после всех midtable-записей, поэтому
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
* столб. */
if (w && h)
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword);
}
/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
* Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри
* тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража
* справа налево от x = 314. Пустые деления у Кида свой спрайт (chtab_2
* id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы
* просто не печатаем полоса и так на чёрном фоне.
*
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
* pop_kid_img_blit; спрайт стража image 0 его chtab (наша страница 0). */
#define HP_Y 194
#define HP_STEP 7
#define HP_KID_FULL 216
#define HP_KID_EMPTY 217
#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */
/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ
* страницах дабл-буфера (иначе на второй останется старое значение и его
* будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг.
*
* Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80
* собирает неверно SUB затирает A, и в s уезжает разность (memory
* sdcc_z80_cmp_store_a_bug). */
static uint8_t hp_todo; /* сколько страниц ещё обновить */
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
* комнату (фон перерисован целиком и стёр её) и при старте. */
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
void pop_hp_draw(void) __banked
{
uint8_t i, n;
uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0);
uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0);
{ /* Изменилось хоть одно число — обновить обе страницы. */
uint8_t k = hitp_curr, km = hitp_max;
uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev ||
gd != hp_gd_prev || gm != hp_gdmax_prev);
hp_kid_prev = k; hp_kidmax_prev = km;
hp_gd_prev = gd; hp_gdmax_prev = gm;
if (changed) hp_todo = 2;
}
if (hp_todo == 0) return;
hp_todo--;
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
for (i = hitp_curr; i < n; i++)
pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF);
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++)
pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF);
/* Страж: только полные деления, справа налево. Скелет полосы не имеет. */
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) {
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
uint16_t w, h;
gfx_w0_map(gp[0].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
/* Атлас стража тоже column-major — блит колоночный. */
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
for (i = 0; i < n; i++)
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
HP_Y + POP_YOFF, img, 0, 0, 0);
}
gfx_w0_unmap();
}
}
+236
View File
@@ -0,0 +1,236 @@
/*
* pop_geom.c общая геометрия + PRNG оригинала. См. pop_geom.h.
*/
#include "pop_geom.h"
/* data.h: шаг 14 (ширина тайла в координатах персонажа), [5] = col 0. */
const int16_t pop_x_bump[20] = {
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
};
const int16_t pop_y_land[5] = {-8, 55, 118, 181, 244};
int8_t pop_y_to_row(int16_t y)
{
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
}
/* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
{
uint16_t n = (uint16_t)(maxv + 1);
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
return (uint16_t)(v & (uint16_t)(n - 1));
return (uint16_t)(v % n);
}
#if POP_PRANDOM_EXACT
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
*
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong 8 430
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
* половины это ~3 500. Здесь ~1 000, при полностью той же
* последовательности, что в SDLPoP. Альтернатива «взять генератор
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
* совместимость с эталоном, по которому сверяем картинку.
*
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 схема Горнера по
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
* 0x343FD) 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
* нужная в конце, попадается по дороге на втором шаге её и сохраняем.
*
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s в статиках
* (регистров на всё не хватает).
*/
uint16_t pop_pr_ptr; /* адрес сида */
uint16_t pop_pr_sh; /* старшее слово исходного сида */
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
{
(void)seed;
__asm
ld (_pop_pr_ptr), hl
ld c, (hl)
inc hl
ld b, (hl) ; BC = s.lo
inc hl
ld a, (hl)
ld (_pop_pr_sh), a
inc hl
ld a, (hl)
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
ld h, b
ld l, c
ld de, (_pop_pr_sh) ; акк = s
;; акк = акк*2 + s => 3s
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
ld (_pop_pr_3s_lo), hl
ld (_pop_pr_3s_hi), de
;; акк = акк*4 + s => 13s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк = акк*16 + s => 209s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк *= 1024 => 214016s (10 удвоений)
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
;; акк -= 3s => 214013s
ld bc, (_pop_pr_3s_lo)
or a ; CY = 0
sbc hl, bc
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
ex de, hl
sbc hl, bc
ex de, hl
;; акк += 2531011 = 0x0026_9EC3
ld bc, #0x9EC3
add hl, bc
ld bc, #0x0026
ex de, hl
adc hl, bc
ex de, hl
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
push de
ex de, hl ; DE = младшее слово
ld hl, (_pop_pr_ptr)
ld (hl), e
inc hl
ld (hl), d
inc hl
pop de ; DE = старшее слово
ld (hl), e
inc hl
ld (hl), d
ret
__endasm;
}
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
return pop_rnd_fit(pop_lcg_step(seed), maxv);
}
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов при двух
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
* умножений нет вовсе, только сдвиги и xor.
*
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
* младшие биты вырождены бит 0 просто чередуется. Наши вызовы это
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
* вместо шума получилась бы правильная шахматка. У xorshift такой
* структуры нет, а стоит он примерно столько же.
*
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
* xorshift ноль неподвижная точка, а сид кладки (номер комнаты + ряд +
* колонка) вполне может оказаться нулём.
*
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP другая (но
* статистически такая же) раскладка кладки и другие броски в боёвке.
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
uint16_t s = seed->lo;
s ^= (uint16_t)(s << 7);
s ^= (uint16_t)(s >> 9);
s ^= (uint16_t)(s << 8);
s = (uint16_t)(s + 0x9E37u);
seed->lo = s;
seed->hi = 0; /* старшая половина не используется */
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
* младших бит, а вызывающие берут от результата именно младшие. */
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
}
#endif
+61
View File
@@ -0,0 +1,61 @@
/*
* pop_geom.h общая геометрия PoP и PRNG оригинала.
*
* Сюда сведено то, что раньше дублировалось по модулям (x_bump/y_land в
* pop_kid и pop_map, y_to_row в pop_bg и pop_map, 32-битный LCG в pop_bg и
* pop_trob). Кроме экономии это ФУНДАМЕНТ bank-safe API: модуль живёт в
* W1/W2 и не трогает графику, поэтому его может звать и главный цикл, и
* резидент W3, и будущий __banked-код стражей (docs/layout_plan_v2.md §4).
*/
#ifndef POP_GEOM_H
#define POP_GEOM_H
#include <stdint.h>
#define FIRST_ONSCREEN_COLUMN 5 /* индекс col 0 комнаты в x_bump */
#define TILE_SIZEX 14 /* ширина тайла в координатах персонажа */
#define TILE_SIZEY 63 /* высота ряда */
/* x_bump (data.h): левая граница колонки в координатах персонажа; колонка
* комнаты col = x_bump[col + FIRST_ONSCREEN_COLUMN]. y_land: пол ряда
* (индекс row+1; [0] ряд «над комнатой»). */
extern const int16_t pop_x_bump[20];
extern const int16_t pop_y_land[5];
/* y_to_row_mod4 (seg006): ряд по координате y с оборотом mod 4 (−1 = полоса
* над комнатой, 3 = под комнатой). */
int8_t pop_y_to_row(int16_t y);
/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
* анимаций тайлов (фаза факелов) свои последовательности, смешивать их
* нельзя.
*
* ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c):
* 1 (по умолчанию) LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же
* раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке
* можно сверяться с эталоном;
* 0 xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на
* 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3,
* 175 кадров: 403 632 против 397 986). Последовательность другая
* статистически эквивалентная, но не та же.
*
* Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит
* умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в
* MAME, breakpoint на __mullong).
*
* ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией
* SDCC те же генераторы на Z80-ассемблере укладываются в 86..148 тактов.
* Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там
* умножение на константу), а менять генератор только если и этого мало. */
typedef struct { uint16_t lo, hi; } pop_rnd_t;
#ifndef POP_PRANDOM_EXACT
#define POP_PRANDOM_EXACT 1
#endif
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);
/* Сид из 16-битного числа (старшая половина = 0). */
#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0)
#endif
+190
View File
@@ -0,0 +1,190 @@
/*
* pop_guard.c СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h.
*
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
* ОТРИСОВКА в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл).
*/
#include <stdint.h>
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_level.h"
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
#include "pop_map.h" /* hitp_* — HP Кида */
#include "kid_data.h"
pop_char_t Guard;
uint8_t guardhp_curr, guardhp_max;
int8_t guardhp_delta;
int8_t can_guard_see_kid;
int8_t is_guard_notice;
uint8_t guard_skill;
int8_t guard_refrac, justblocked, kid_sword_strike;
int8_t offguard, holding_sword;
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
* см. pop_geom.h иначе перерисовка стены сбивала бы броски стража. */
pop_rnd_t pop_fight_seed;
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
kframe pop_gframe;
uint8_t pop_guard_present(void)
{
return (uint8_t)(Guard.charid != 0 && Guard.alive < 0 && guardhp_curr != 0);
}
void pop_guard_reset(void)
{
Guard.charid = 0;
Guard.alive = 0;
Guard.direction = 0;
guardhp_curr = 0;
guardhp_delta = 0;
can_guard_see_kid = 0;
is_guard_notice = 0;
guard_refrac = justblocked = kid_sword_strike = 0;
offguard = holding_sword = 0;
}
/* kill guard cheat (seg000:786):
* if (Guard.charid != charid_4_skeleton) {
* guardhp_delta = -guardhp_curr;
* Guard.alive = 0;
* }
* Скелета чит не убивает он и в оригинале бессмертен. */
void pop_guard_kill(void)
{
if (Guard.charid == CHARID_4_SKELETON) return;
if (!pop_guard_present()) return;
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
* общий control() ставит ему seq_71_dying. */
guardhp_delta = (int8_t)-guardhp_curr;
Guard.alive = 0;
}
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
void pop_do_delta_hp(void)
{
int16_t hp;
if (hitp_delta) {
hp = (int16_t)hitp_curr + hitp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
hitp_curr = (uint8_t)hp;
pop_kid_hurt = 1; /* -> «брызги» поверх Кида */
hitp_delta = 0;
}
if (guardhp_delta) {
hp = (int16_t)guardhp_curr + guardhp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
guardhp_curr = (uint8_t)hp;
guardhp_delta = 0;
}
}
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535). */
void pop_guard_load_frame(void)
{
int16_t idx = (int16_t)Guard.frame;
if (idx >= 102 && idx < 107) idx += 70; /* add_frame */
idx -= 149;
if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; }
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
}
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
* последовательностью тогда при возврате он лежит там же, а не встаёт
* живым. */
void pop_guard_leave(void)
{
if (Guard.charid == 0) return; /* стража не было */
pop_guard_state_save(Guard.room,
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
Guard.direction, Guard.x, guard_skill,
Guard.curr_seq,
(uint8_t)(Guard.alive >= 0));
}
/* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа —
* floor((xpos 7 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */
static int8_t guard_col_from_x(uint8_t xpos)
{
int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN];
int16_t col = x / TILE_SIZEX;
if (x % TILE_SIZEX < 0) --col;
return (int8_t)col;
}
void pop_guard_enter(uint8_t room)
{
uint8_t tile, color, skill;
int8_t dir;
pop_guard_reset();
/* load_lev_spr (seg000:1112): tbl_guard_type == 1 — стражей на уровне
* НЕТ вообще (14/15), их графика даже не грузится. Без этой проверки
* на таком уровне полезли бы стражи из данных комнат. */
if (pop_level_guard_type(pop_current_level) < 0) return;
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
* колонки тайла, а не берётся из файла. */
Guard.room = room;
Guard.curr_row = (int8_t)(tile / 10);
Guard.y = (uint8_t)pop_y_land[Guard.curr_row + 1];
Guard.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Guard.curr_col = (int8_t)(tile % 10);
Guard.direction = dir;
Guard.charid = CHARID_2_GUARD;
Guard.sword = 0; /* sword_0_sheathed */
Guard.alive = -1;
Guard.action = 1; /* actions_1_run_jump */
Guard.fall_x = Guard.fall_y = 0;
/* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
* всё, что вне диапазона, оригинал сводит к 3. */
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
* extrastrength (seg002:38) ненулевой только у skill 4 (=1). */
guardhp_max = guardhp_curr =
(uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0));
guardhp_delta = 0;
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность он лежит
* ровно там, где лёг (порт ветки guards_seq_hi != 0). */
{
uint16_t saved_seq = pop_guard_state_seq(room);
/* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620)
* пересчитывает её из колонки только при загрузке уровня, а дальше
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
* тайла. */
if (saved_seq) Guard.x = pop_guard_state_x(room);
/* ...и КОЛОНКУ пересчитываем из X (enter_guard, seg002:180:
* `Char.curr_col = get_tile_div_mod_m7(Char.x)`). Брать её из
* тайла нельзя: leave_guard сохраняет get_tilepos(0, row), то
* есть колонку 0 всегда. У живого стража это незаметно (X там
* сам считается из колонки), а у ЗАПОМНЕННОГО ТРУПА X
* настоящая, и curr_col оставался нулём при теле, лежащем в
* колонке 2. Тайл трупа шёл в обходе раньше тайла Кида, и Кид
* рисовался ПОВЕРХ тела вместо того, чтобы пройти за ним
* (BUG-DRAWORDER-1). Колонка нужна не только отрисовке на
* неё смотрят коллизия и check_can_guard_see_kid. */
Guard.curr_col = guard_col_from_x(Guard.x);
pop_loadshad();
if (saved_seq) Char.curr_seq = saved_seq;
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
pop_saveshad();
pop_guard_play(); /* до первого кадра */
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
Guard.alive = 1;
guardhp_curr = 0;
}
}
}
+169
View File
@@ -0,0 +1,169 @@
/*
* pop_guard.h СОСТОЯНИЕ стража и точки входа в его логику.
*
* Разделение по окнам (docs/layout_plan_v2.md §4, подтверждено пробником
* tests/w3bankgfx):
* - СОСТОЯНИЕ живёт здесь, в W1/W2. Писучие статики __banked-модуля
* линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк
* только код;
* - ЛОГИКА (ИИ, боёвка) в guards.c, собираемом как --bank 1=guards.c;
* - ОТРИСОВКА стража в pop_gdraw.c, РЕЗИДЕНТЕ W3 (--w3): её зовёт
* только главный цикл, а из банка резидент недостижим. Так код
* отрисовки не занимает дефицитные W1/W2 (куча).
*
* Поля char_type как в оригинале (types.h:302), чтобы порт seg005/seg006
* ложился один в один.
*/
#ifndef POP_GUARD_H
#define POP_GUARD_H
#include <stdint.h>
#include "kid_data.h" /* kframe */
#include "pop_char.h"
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
/* seqids (types.h:1052+) — последовательности боя и стойки. */
#define SEQ_55_DRAW_SWORD 55
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
#define SEQ_57_BACK_WITH_SWORD 57
#define SEQ_58_GUARD_STRIKE 58
#define SEQ_60_TURN_WITH_SWORD 60
#define SEQ_61_PARRY_AFTER_STRIKE 61
#define SEQ_62_PARRY 62
#define SEQ_66_STRIKE_AFTER_PARRY 66
#define SEQ_71_DYING 71
#define SEQ_75_STRIKE 75 /* удар (Кид) */
#define SEQ_77_GUARD_STAND_INACTIVE 77
#define SEQ_80_STAND_FLIPPED 80
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
#define SEQ_87_GUARD_BECOME_INACT 87
#define SEQ_90_EN_GARDE 90
#define SEQ_92_PUT_SWORD_AWAY 92
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
/* frameids (types.h:911+) и состояния меча (types.h:332). */
#define FRAME_150_PARRY 150
#define FRAME_151_STRIKE_1 151
#define FRAME_152_STRIKE_2 152
#define FRAME_153_STRIKE_3 153
#define FRAME_157_WALK_WITH_SWORD 157
#define FRAME_158_STAND_WITH_SWORD 158
#define FRAME_161_PARRY 161
#define FRAME_162_BLOCK_TO_STRIKE 162
#define FRAME_165_WALK_WITH_SWORD 165
#define FRAME_166_STAND_INACTIVE 166
#define FRAME_167_BLOCKED 167
#define FRAME_168_BACK 168
#define FRAME_169_BEGIN_BLOCK 169
#define FRAME_170_STAND_WITH_SWORD 170
#define FRAME_171_STAND_WITH_SWORD 171
#define SWORD_0_SHEATHED 0
#define SWORD_2_DRAWN 2
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
#define CHARID_0_KID 0
#define CHARID_1_SHADOW 1
#define CHARID_2_GUARD 2
#define CHARID_4_SKELETON 4
#define CHARID_5_PRINCESS 5
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
#define NUM_GUARD_SKILLS 12
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
extern uint8_t guardhp_curr, guardhp_max;
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
* 0 не видит, 1 видит, но не пойдёт (между ними loose/чомпер/дыра),
* 2 видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
extern int8_t can_guard_see_kid;
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
* снимает сам ИИ. */
extern int8_t is_guard_notice;
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
* кадр в autocontrol_opponent как в оригинале.
* guard_skill 0..11, из данных уровня; индекс в таблицы вероятностей;
* guard_refrac «отдышка» стража: пока >0, он не атакует и не наступает;
* justblocked Кид только что парировал (влияет на вероятность блока);
* kid_sword_strike Кид только что бил (стража это придерживает). */
extern uint8_t guard_skill;
extern int8_t guard_refrac, justblocked, kid_sword_strike;
/* Состояние меча КИДА (seg005/seg006):
* offguard меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
* достаёт клинок автоматически, даже видя стража;
* holding_sword клинок в руке (для индикатора HP стража). */
extern int8_t offguard, holding_sword;
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
extern pop_rnd_t pop_fight_seed;
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
void pop_check_can_guard_see_kid(void) __banked;
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
void pop_check_sword_hurting(void) __banked;
void pop_check_sword_hurt(void) __banked;
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
* читает отрисовка в резиденте W3. image == 255 рисовать нечего. */
extern kframe pop_gframe;
void pop_guard_load_frame(void);
/* Есть ли живой страж в отрисованной комнате (для главного цикла). */
uint8_t pop_guard_present(void);
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
* 0 OK, -1 ошибка (тогда страж просто не рисуется). */
int pop_guard_load(void) __banked;
void pop_guard_free(void) __banked;
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
* seg002:0112) либо убрать его, если в комнате стража нет. */
void pop_guard_enter(uint8_t room);
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
* enter_room, пока Guard ещё держит прежнего. */
void pop_guard_leave(void);
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
* как kid_draw) и стереть прошлый (heal). Реализация pop_gdraw.c
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
* ПОСЛЕ персонажей она ничего не перекрывает. */
void pop_hp_draw(void) __banked;
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
void pop_hp_invalidate(void) __banked;
void pop_guard_heal(void) __banked;
void pop_guard_draw(void) __banked;
/* Ширина спрайта последнего нарисованного кадра стража (char_width_half
* в set_char_collision). Нужна для его ТАЙЛА ОБЪЕКТА от него зависит
* порядок отрисовки относительно Кида. */
uint16_t pop_guard_fp_width(void) __banked;
/* Убрать стража со сцены (смена комнаты / инициализация). */
void pop_guard_reset(void);
/* Чит «убить стража» (seg000:786, K): скелета не берёт. Вызывать только
* при включённых читах проверку делает вызывающий. */
void pop_guard_kill(void);
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале в play_frame). */
void pop_do_delta_hp(void);
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
void pop_guard_tick(void) __banked;
#endif
+621 -60
View File
@@ -2,36 +2,135 @@
* pop_kid.c движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
* seqtbl-интерпретатор. Данные kid_data.h.
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include "kid_data.h"
kid_t Kid;
pop_char_t Kid;
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
* в play_seq, потребляется pop_check_knock (pop_map) трясёт loose-полы. */
int8_t knock;
/* Порт глобала next_level: seqtbl-команда END_LEVEL инкрементит его в конце
* seq_70 (подъём по лестнице за дверью уровня), а главный цикл сравнивает с
* текущим уровнем и грузит следующий. Счётчик, а не флаг ровно как в
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
* легли сюда же без переделки. */
uint8_t pop_next_level;
/* Координатные таблицы (data.h). */
static const uint8_t x_bump[20] = {
(uint8_t)-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
};
static const int16_t y_land[5] = {-8, 55, 118, 181, 244};
#define FIRST_ONSCREEN_COLUMN 5
#define TILE_SIZEX 14
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
#define KID_MAXPAGES 28
static atlas_t kidp[KID_MAXPAGES];
static uint8_t kid_npages;
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
* ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
* Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
* sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
* Смещения в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
* calc_screen_x_coord, x «вперёд» по направлению персонажа. */
static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
static atlas_t swordp;
static uint8_t sword_ok;
int pop_sword_load(void)
{
if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
sword_ok = 1;
return 0;
}
/* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в
* кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
* в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем
* замапленной вызывающий делает gfx_w0_unmap сам. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh)
{
uint8_t sf, id, flip;
const uint8_t *simg;
uint16_t w, h;
int bx, top;
if (!sword_ok) return 0;
if (!((ch->frame >= 229 && ch->frame < 238) ||
ch->sword != 0 ||
(ch->charid == CHARID_2_GUARD && ch->alive < 0)))
return 0;
sf = (uint8_t)(fr->sword & 0x3F);
if (sf == 0 || sf >= KID_NSWORD) return 0;
id = SWORD_ID[sf];
if (id == 0xFF) return 0;
simg = (const uint8_t *)atlas_image(&swordp, id);
gfx_w0_map(swordp.page);
w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (!w || !h) return 0;
flip = (uint8_t)(ch->direction >= 0);
bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE);
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0);
else
gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
*rx = bx; *ry = top; *rw = w; *rh = h;
return 1;
}
/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y).
* Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры
* Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */
void pop_kid_img_blit(uint8_t image, int x, int y)
{
uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */
uint8_t idx = (uint8_t)(image & 7);
const uint8_t *img;
uint16_t w, h;
if (pg >= kid_npages) return;
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
gfx_w0_map(kidp[pg].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
/* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid —
* ради бесплатного горизонтального флипа), поэтому блит обязан быть
* колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной.
* noclip БЕЗ проверки (CLIP-1): единственный вызывающий полоса HP
* (pop_gdraw.c), у неё координаты фиксированные и заведомо на экране. */
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
gfx_w0_unmap();
}
int pop_kid_load(uint8_t npages)
{
static const char *const names[KID_MAXPAGES] = {
"kid0.atl","kid1.atl","kid2.atl","kid3.atl","kid4.atl","kid5.atl",
"kid6.atl","kid7.atl","kid8.atl","kid9.atl","kid10.atl","kid11.atl",
"kid12.atl","kid13.atl","kid14.atl","kid15.atl","kid16.atl","kid17.atl",
"kid18.atl","kid19.atl","kid20.atl","kid21.atl","kid22.atl","kid23.atl",
"kid24.atl","kid25.atl","kid26.atl","kid27.atl"
"KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
"KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
"KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
"KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
"KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
};
uint8_t i;
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
@@ -50,99 +149,371 @@ void pop_kid_free(void)
kid_npages = 0;
}
/* ---- seqtbl-доступ (база SEQTBL_BASE) ------------------------------ */
#define SEQ(cs) kid_seqtbl[(uint16_t)((cs) - SEQTBL_BASE)]
/* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
* весь остальной код читает cur_frame 5-байтовую копию текущего кадра.
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
* и коллизия/отрисовка работают с ним, а не с таблицей.
* Страница адресуется как у pop_level: данные с 0x100, ниже ISR-стаб. */
extern void _gfx_w0_isr(void);
#define KD_DATA_OFF 0x100u
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
* НЕВЕРНО умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
* берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
* (`ld c,l` / `inc b`). Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
* 256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
* пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
* Артефакт: .sprinter-cc-roomtest/pop_kid.asm, функция load_frame. */
static const uint8_t *kd_frame_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2); /* i*5 = i + i*4, без умножения */
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
return (const uint8_t *)off;
}
/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
* страницу данных, см. pop_extract_kid_data.py). */
static const uint8_t *kd_gframe_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2);
off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
return (const uint8_t *)off;
}
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
(uint16_t)((cs) - SEQTBL_BASE)))
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
* страницей данных то есть внутри play_seq. */
#define SEQ(cs) KD_SEQ(cs)
static uint8_t kdat_blk, kdat_page, kdat_ok;
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
* тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
static kframe cur_frame;
static kframe kid_frame; /* кадр Kid для kid_draw */
/* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в
* оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
* стоят loadkid/savekid и loadshad/saveshad (seg006:809..825). Копия, а
* не указатель, и так же, как в оригинале, и быстрее на Z80: обращения
* в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
* копирование платится один раз на переключение персонажа. */
pop_char_t Char;
/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
* рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
* ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
* ними: для стража Opp = Kid, для Кида Opp = Guard. */
pop_char_t Opp;
/* Снимок последовательности на момент входа в окно Char: по нему
* pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)»
* от «её сменил pop_map (прямо в Kid)». */
static uint16_t kid_seq_at_load;
void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; }
void pop_savekid(void) { Kid = Char; }
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально
* меняет у активного персонажа последовательность и состояние меча.
*
* Почему не `Kid = Char`, как делает savekid оригинала: у нас часть
* действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq,
* safe_step...), и он пишет ПРЯМО в Kid там ещё не портирован переход на
* Char. Полное копирование затирало эти правки: Kid терял выравнивание x
* и ряд, и спуск с уступа через вис не срабатывал персонаж просто
* приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */
void pop_savekid_state(void)
{
/* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char
* (pop_char_set_seq), pop_map прямо в Kid (kid_set_seq: спуск с
* уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше:
* диспетчер про эти переходы не знает и, копируя Char вслепую, затирал
* их спуск в проём не срабатывал, а персонаж даже не приседал, потому
* что pop_down_action уже вернул «обработано». */
uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load);
if (!map_changed) Kid.curr_seq = Char.curr_seq;
Kid.sword = Char.sword;
}
void pop_loadshad(void) { Char = Guard; }
void pop_saveshad(void) { Guard = Char; }
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; }
void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; }
/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
* +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
* приходится на ширину тайла между ними. Живёт здесь, а не в guards.c,
* потому что нужен по обе стороны банковой границы и ИИ, и диспетчеру
* боёвки (pop_ctrl). */
int16_t pop_char_opp_dist(void)
{
int16_t distance;
if (Char.room != Opp.room) return 999;
distance = (int16_t)Opp.x - (int16_t)Char.x;
if (Char.direction < 0) distance = -distance;
if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
return distance;
}
int pop_kid_data_load(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
kdat_blk = blk;
kdat_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(kdat_page);
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
pg[0x38] = 0xC3;
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
kdat_ok = 1;
return 0;
}
void pop_kid_data_free(void)
{
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
}
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
* разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF). Сама маппит страницу,
* поэтому годится для вызова извне (pop_guard) но НЕ в горячем цикле. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
{
const uint8_t *f;
uint16_t off = (uint16_t)idx;
if (!kdat_ok) { out5[0] = 255; return; }
off += (uint16_t)(off << 2); /* idx*5, без умножения */
off += (uint16_t)(KD_DATA_OFF + tbl_off);
gfx_w0_map(kdat_page);
f = (const uint8_t *)off;
out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
out5[3] = f[3]; out5[4] = f[4];
gfx_w0_unmap();
}
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
static void load_frame(void)
{
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
* стража frame_tbl_guard с индексом frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535). */
const uint8_t *f;
if (Char.charid == CHARID_2_GUARD) {
int16_t idx = (int16_t)Char.frame;
if (idx >= 102 && idx < 107) idx += 70;
idx -= 149;
if (idx < 0 || idx >= KID_NGFRAMES) { cur_frame.image = 255; return; }
f = kd_gframe_ptr((uint8_t)idx);
} else {
f = kd_frame_ptr(Char.frame);
}
cur_frame.image = f[0];
cur_frame.dx = (int8_t)f[1];
cur_frame.dy = (int8_t)f[2];
cur_frame.flags = f[3];
cur_frame.sword = f[4];
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
* на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа
* Кида и рисовал произвольную позу. Теперь владелец один тот, для
* кого кадр реально загружен. */
if (Char.charid == CHARID_0_KID) kid_frame = cur_frame;
else pop_gframe = cur_frame;
}
static uint8_t char_dx_forward(int8_t dx)
{
if (Kid.direction < 0) dx = (int8_t)-dx;
return (uint8_t)(Kid.x + dx);
if (Char.direction < 0) dx = (int8_t)-dx;
return (uint8_t)(Char.x + dx);
}
/* Аксессоры к текущему кадру (для pop_map — без дублирования kid_frames). */
int8_t kid_cur_dx(void) { return kid_frames[Kid.frame].dx; }
uint8_t kid_cur_flags(void) { return kid_frames[Kid.frame].flags; }
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
int8_t kid_cur_dx(void) { return cur_frame.dx; }
int8_t kid_cur_dy(void) { return cur_frame.dy; }
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0). */
/* То же для АКТИВНОГО персонажа — внутри окна Char (порт
* seqtbl_offset_char, seg006). Нужен стражу: его последовательность
* ставится, когда Char уже загружен из Guard. */
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
* Страница данных маппится в W0 на ВЕСЬ разбор один map/unmap за вызов
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце
* load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
void play_seq(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
for (;;) {
uint8_t cmd = SEQ(Kid.curr_seq);
Kid.curr_seq++;
uint8_t cmd = SEQ(Char.curr_seq);
Char.curr_seq++;
switch (cmd) {
case 0xFB: /* DX */
Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq));
Kid.curr_seq++;
Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
Char.curr_seq++;
break;
case 0xFA: /* DY */
Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq));
Kid.curr_seq++;
Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
Char.curr_seq++;
break;
case 0xFE: /* FLIP */
Kid.direction = (int8_t)~Kid.direction;
Char.direction = (int8_t)~Char.direction;
break;
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
Kid.curr_seq += 2;
Char.curr_seq += 2;
break;
case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) |
((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8));
Kid.curr_seq = a;
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
Char.curr_seq = a;
break;
}
case 0xFD: /* UP */
Kid.curr_row--;
Char.curr_row--;
break;
case 0xFC: /* DOWN */
Kid.curr_row++;
Char.curr_row++;
break;
case 0xF9: /* ACTION */
Kid.action = SEQ(Kid.curr_seq);
Kid.curr_seq++;
Char.action = SEQ(Char.curr_seq);
Char.curr_seq++;
break;
case 0xF8: /* SET_FALL */
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
break;
case 0xF5: case 0xF4: /* KNOCK up/down */
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
knock = 1;
break;
case 0xF2: /* SOUND — потребить 1 байт */
Kid.curr_seq++;
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
knock = -1;
break;
case 0xF1: /* END_LEVEL */
case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт
* ВТОРОЙ смысл «Кид нашумел», и его терять нельзя.
* Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0
* назван silent потому, что НЕ звучит, а стражи его всё
* равно замечают (стоит, например, в seq доставания меча).
* DRINK(3)/LEVEL(4) не шум. См. BUG-GUARD-DEAF-1. */
if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1;
Char.curr_seq++;
break;
case 0xF3: /* GET_ITEM — потребить 1 байт */
Kid.curr_seq++;
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
* В оригинале это `++next_level`, а главный цикл
* (play_level_2, seg003:0386) выходит, когда next_level
* разошёлся с current_level. У нас так же: счётчик, а
* решение снаружи (roomtest.c). */
pop_next_level++;
break;
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
* (порт play_seq -> proc_get_object, seg006) */
if (SEQ(Char.curr_seq) == 1)
pop_proc_get_object();
Char.curr_seq++;
break;
case 0xF6: /* DIE nop */
break;
default: /* КАДР */
Kid.frame = cmd;
Char.frame = cmd;
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
gfx_w0_unmap();
return;
}
}
}
/* load_fram_det_col (seg006:0144) = load_frame() + determine_col().
*
* Зачем это отдельной публичной точкой. cur_frame ОДИН глобал на всех
* персонажей (так и в оригинале), и его владелец тот, кто последним
* прошёл load_frame. В кадре последним тикает страж, поэтому к моменту
* управления Кидом в cur_frame лежит кадр СТРАЖА. А через cur_frame
* считается вся геометрия: dx_weight -> determine_col ->
* distance_to_edge_weight -> get_edge_distance, то есть буквально выбор
* ветки в check_jump_up.
*
* Оригинал от этого страхуется явно: play_kid_frame (seg000:1209) и
* play_guard_frame (seg000:1246) сразу после loadkid/loadshad зовут
* load_fram_det_col() ДО control(). У нас этого не было, и Кид считал
* свой прыжок по кадру стража (BUG-CTRL-FRAME-1): при distance 2 вместо 10
* check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и
* зацеп». Результат менялся от кадра к кадру вместе с анимацией стража.
*
* determine_col у нас существует только для Кида: pop_map работает с Kid
* напрямую, а не с абстрактным Char. Поэтому вторую половину связки делаем
* только на его ветке стражу пока хватает перезагрузки самого кадра (его
* колонку ведёт pop_guard_enter/leave). Char.curr_col синхронизируем
* руками: окно Char копия Kid. */
void pop_load_fram_det_col(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
load_frame();
gfx_w0_unmap();
if (Char.charid == CHARID_0_KID) {
pop_determine_col();
Char.curr_col = Kid.curr_col;
}
}
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
* работает с Char и сам по себе не знает, чей это персонаж. */
void pop_kid_play(void) { pop_loadkid(); play_seq(); pop_savekid(); }
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
{
Kid.curr_col = col;
Kid.curr_row = row;
Kid.direction = dir;
Kid.x = (uint8_t)(x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Kid.y = (uint8_t)y_land[row + 1];
Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Kid.y = (uint8_t)pop_y_land[row + 1];
Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0;
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
Kid.curr_seq = kid_seq_off[seq_id];
play_seq(); /* до первого кадра */
pop_kid_play(); /* до первого кадра */
}
void kid_tick(void)
{
play_seq();
pop_kid_play();
}
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
@@ -172,35 +543,159 @@ static int kid_lx[2], kid_ly[2];
static uint16_t kid_lw[2], kid_lh[2];
static uint8_t kid_lvalid[2];
/* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона).
* Раньше он объединялся с прямоугольником персонажа но объединение сильно
* больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил
* почти вдвое шире и выше самого Kid, а heal стоит ровно по площади.
* Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение
* оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования
* зато клинок/брызги честно уходят под передние грани тайлов). */
static int kid_ox[2], kid_oy[2];
static uint16_t kid_ow[2], kid_oh[2];
static uint8_t kid_ovalid[2];
/* Добавить накладной спрайт в overlay-прямоугольник страницы dp. Клинок и
* брызги в одном кадре встречаются редко и оба маленькие их объединение
* дешевле третьего слота. */
static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h)
{
if (!kid_ovalid[dp]) {
kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h;
kid_ovalid[dp] = 1;
return;
}
{
int x0 = kid_ox[dp], y0 = kid_oy[dp];
int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_oh[dp];
if (x < x0) x0 = x;
if (y < y0) y0 = y;
if (x + (int)w > x1) x1 = x + (int)w;
if (y + (int)h > y1) y1 = y + (int)h;
kid_ox[dp] = x0; kid_oy[dp] = y0;
kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0);
}
}
/* Окно fore-клипа = объединение «персонаж + накладные». Считается по
* прямоугольникам, ничего не рисует. */
static void kid_fore_clip(uint8_t dp)
{
int x0, y0, x1, y1;
if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; }
if (!kid_ovalid[dp]) {
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
(int)kid_lw[dp], (int)kid_lh[dp]);
return;
}
if (!kid_lvalid[dp]) {
pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF,
(int)kid_ow[dp], (int)kid_oh[dp]);
return;
}
x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp];
y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp];
x1 = kid_lx[dp] + (int)kid_lw[dp];
y1 = kid_ly[dp] + (int)kid_lh[dp];
if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp];
if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp];
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
}
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
* obj_x ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y низ, w/h размер. */
static int kid_fp_x, kid_fp_y;
static uint16_t kid_fp_w, kid_fp_h;
/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room):
* 140 когда комната Kid отрисованной; 0 обычно. */
static int kid_render_dx;
void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; }
int kid_fp_obj_x(void) { return kid_fp_x; }
int kid_fp_obj_y(void) { return kid_fp_y; }
uint16_t kid_fp_width(void) { return kid_fp_w; }
uint16_t kid_fp_height(void) { return kid_fp_h; }
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт
* chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27).
* Позиция от кадра Kid: obj_y = 15, obj_x += 5 ВПЕРЁД (obj_dx_forward).
* Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают
* иначе тут только общий случай + «на пиках» (порт по мере надобности).
* Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */
#define KID_SPLASH_IMAGE 218
void kid_draw_splash(void)
{
int obj_x = kid_fp_x, obj_y = kid_fp_y - 15;
uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7;
const uint8_t *img;
uint16_t w, h;
if (page >= kid_npages) return;
if (Kid.frame == 177) /* напоролся на пики */
obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5;
else
obj_x += (Kid.direction < 0) ? -5 : 5;
obj_x = obj_x * 8 / 7; /* лог. X -> экран (calc_screen_x_coord) */
img = (const uint8_t *)atlas_image(&kidp[page], idx);
gfx_w0_map(kidp[page].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
uint8_t flip = (Kid.direction >= 0);
int bx = obj_x;
int top = obj_y - (int)h + 1;
if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
else
gfx_blit_cols(bx, top + POP_YOFF, img, flip);
gfx_set_bank(GFX_BANK_NORMAL);
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
/* Брызги — накладной спрайт: свой прямоугольник heal (см.
* kid_overlay_add), окно fore пересчитываем объединением. */
{
uint8_t dp = gfx_get_draw_page();
kid_overlay_add(dp, bx, top, w, h);
kid_fore_clip(dp);
}
}
gfx_w0_unmap();
}
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid.
*
* pop_heal_fast, а не голый gfx_heal: прямоугольник тут это ровно тот
* прямоугольник, который kid_draw нарисовал noclip-ядром, то есть в
* подавляющем большинстве кадров он заведомо на экране. Клипающий путь
* остаётся для straddle (kid_render_dx = 140) и падения ниже поля
* там координата реально уходит за экран. */
void kid_heal(void)
{
uint8_t p = gfx_get_draw_page();
if (kid_lvalid[p])
gfx_heal(kid_lx[p], kid_ly[p], kid_lw[p], kid_lh[p]);
kid_lvalid[p] = 0;
if (kid_lvalid[p]) /* +YOFF: центрирование */
pop_heal_fast(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]);
if (kid_ovalid[p])
pop_heal_fast(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]);
kid_lvalid[p] = kid_ovalid[p] = 0;
}
void kid_draw(void)
{
kframe fr = kid_frames[Kid.frame];
kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */
int fwd, obj_x, obj_y, top;
uint8_t page, idx;
const uint8_t *img;
uint16_t w, h;
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
* кадра / страница не загружена) перекрывать нечего, и fore-проход
* не должен трогать экран. */
pop_fore_set_clip(0, 0, 0, 0);
if (fr.image == 255)
return; /* пустой кадр */
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx);
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
* отрисованной спрайт за кромкой. Коллизия (Kid.x в pop_map) НЕ
* смещается: она в кадре комнаты Kid; смещение только рендерное. */
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx;
obj_x = (fwd << 1) - 116;
obj_y = (int)Kid.y + fr.dy;
if ((int8_t)(fr.flags ^ Kid.direction) >= 0)
@@ -224,16 +719,82 @@ void kid_draw(void)
if (w && h) {
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
int bx = obj_x + KID_XOFF;
int skip = 0; /* строк спрайта срезано сверху */
int ct, cr;
uint16_t vis_w = w; /* видимая ширина (клип справа) */
if (flip)
bx -= (int)w; /* seg008: xpos -= image->w */
top = obj_y - (int)h + 1 + KID_YOFF;
uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */
/* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше
* линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает
* сквозь щели кладки при спуске/висе. */
ct = pop_clip_char_top(kid_fp_x, obj_y, w, h);
if (ct > top) {
skip = ct - top;
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
}
/* clip_char, вторая половина: правый косяк двери уровня. Дверь
* рисуется ПОД персонажем (вся в backtable), поэтому спрятать его
* можно только срезав спрайт см. pop_clip_char_right. */
cr = pop_clip_char_right();
if (cr) {
int avail = cr - bx;
if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */
else if (avail < (int)w) vis_w = (uint16_t)avail;
}
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
gfx_blit_cols(bx, top, img, flip);
if (skip < (int)h && vis_w) { /* +YOFF: центрирование */
/* noclip-ядро ширину не ограничивает, поэтому при обрезке справа
* идём общим путём: это редкие кадры (подъём в двери уровня раз
* за уровень), цена подготовки тут не видна. */
if (vis_w == w && pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
(uint16_t)((int)h - skip)))
gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip,
(uint8_t)skip, 0);
else
gfx_blit_cols_part_w(bx, top + skip + POP_YOFF, img, flip,
skip, 0, (uint8_t)(vis_w == w ? 0 : vis_w));
}
gfx_set_bank(GFX_BANK_NORMAL);
kid_lx[dp] = bx; kid_ly[dp] = top;
kid_lw[dp] = w; kid_lh[dp] = h; kid_lvalid[dp] = 1;
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
* узко, только по прямоугольнику Кида и только при выходе. */
pop_clip_sprite(bx, w, top + skip + POP_YOFF, (int)h - skip);
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
kid_lx[dp] = bx; kid_ly[dp] = top + skip;
kid_lw[dp] = vis_w; kid_lh[dp] = (uint16_t)((int)h - skip);
kid_lvalid[dp] = (skip < (int)h && vis_w != 0);
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
}
gfx_w0_unmap();
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
* Kid со СВОИМ прямоугольником heal (kid_overlay_add). */
{
int sx, sy; uint16_t sw, sh;
if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
}
gfx_w0_unmap();
/* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
* fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
* тайлами см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
* нулевое окно = fore-проход не трогает экран, что и правильно. */
kid_fore_clip(gfx_get_draw_page() & 1);
}
/* Вернуть окно fore-клипа на прямоугольник ПОСЛЕДНЕГО кадра Kid.
*
* Окно (pop_fore_set_clip) одно на всех, и его ставит КАЖДЫЙ, кто рисует
* персонажа: kid_draw в конце своё, pop_guard_draw в конце своё. Пока
* страж рисовался строго ДО Кида, к моменту pop_fore_over_kid в окне
* оказывался Kid, и всё сходилось само. Как только порядок стал
* переменным (guard_over_kid в roomtest.c порядок задаёт обход тайлов),
* при «страж поверх» окно оставалось СТРАЖЬИМ и fore-проход Кида отсекался
* целиком: Кид рисовался поверх передних столбов.
*
* Поэтому окно восстанавливается явно перед pop_fore_over_kid, а не
* держится «по счастливому порядку вызовов». */
void kid_fore_clip_restore(void)
{
kid_fore_clip(gfx_get_draw_page() & 1);
}
+96 -13
View File
@@ -11,26 +11,53 @@
#define POP_KID_H
#include <stdint.h>
#include "pop_char.h"
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
/* Состояние персонажа (подмножество char_type, types.h). */
typedef struct {
uint8_t frame;
uint8_t x, y;
int8_t direction; /* -1 влево, 0 вправо */
int8_t curr_col, curr_row;
uint8_t action;
int8_t fall_x, fall_y;
uint8_t repeat;
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
} kid_t;
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
extern pop_char_t Kid;
extern kid_t Kid;
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
extern int8_t knock;
/* Порт next_level (seg000): END_LEVEL в play_seq инкрементит — «уровень
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
* там рестарт того же уровня; загрузка следующего levels_plan.md. */
extern uint8_t pop_next_level;
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
* 0 OK, -1 ошибка. */
int pop_kid_load(uint8_t npages);
/* Данные анимации (таблица кадров + seqtbl) — отдельная EMM-страница
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
* первого kid_init/play_seq. 0 OK, -1 ошибка (без них Kid не оживёт).
* Страница маппится в W0 внутри play_seq, раз за тик. */
int pop_kid_data_load(const char *path);
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
* Маппит страницу сама; для горячего цикла есть cur_frame. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5);
void pop_kid_data_free(void);
/* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах
* подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798).
* Грузить ДО initgraph, как и страницы Kid. 0 OK, -1 ошибка (тогда
* кадры рисуются без клинка). */
int pop_sword_load(void);
void pop_kid_free(void);
/* Нарисовать клинок chtab_0 поверх персонажа (порт add_sword_to_objtable,
* seg006:1798) общая точка для Кида и стража: меч в оригинале ОДИН на
* всех. obj_x/obj_y та же опорная точка, что у спрайта персонажа.
* Возвращает 1 и прямоугольник клинка (для heal), 0 клинка нет.
* Страницу W0 оставляет замапленной: вызывающий делает gfx_w0_unmap. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh);
/* Инициализировать Kid: последовательность seq_id, клетка col/row,
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
@@ -41,21 +68,77 @@ void kid_tick(void);
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
* physics-шаг, как оригинал). */
void play_seq(void);
void play_seq(void); /* работает с Char — см. pop_kid_play ниже */
/* Активный персонаж (порт Char, seg006) и его загрузка/выгрузка.
* Интерпретатор последовательностей общий для Kid и стража: у них общая
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
extern pop_char_t Char;
/* Соперник активного персонажа (порт Opp): ставится вместе с Char через
* pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */
extern pop_char_t Opp;
void pop_loadkid(void);
void pop_savekid(void);
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
void pop_loadshad(void);
void pop_saveshad(void);
void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */
void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */
void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */
void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */
/* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной,
* 999 разные комнаты (порт char_opp_dist, seg006:2135). */
int16_t pop_char_opp_dist(void);
/* Прокрутить последовательность персонажа целиком (load + play_seq +
* save). Звать вместо голого play_seq. */
void pop_char_set_seq(uint8_t seq_id); /* seq активного персонажа */
void pop_kid_play(void);
void pop_guard_play(void);
/* load_fram_det_col (seg006:0144) — перезагрузить cur_frame под АКТИВНОГО
* персонажа (Char) и пересчитать его колонку. Звать сразу после pop_loadkid/
* pop_loadshad и ДО диспетчера control(), как это делают play_kid_frame /
* play_guard_frame (seg000:1209/1246): cur_frame один на всех, и без этого
* геометрия одного персонажа считается по кадру другого. */
void pop_load_fram_det_col(void);
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
* таблицы во втором TU). */
int8_t kid_cur_dx(void);
int8_t kid_cur_dy(void);
uint8_t kid_cur_flags(void);
/* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая
* точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */
void kid_set_seq(uint8_t seq_id);
/* Рендер-смещение Kid по ЛОГИЧЕСКОЙ X (порт xpos_in_drawn_room, seg004):
* когда комната Kid отрисованной (straddle у шва), спрайт рисуется со
* сдвигом 140 (левый сосед 140 / правый +140), при этом Kid.x/коллизия
* остаются в кадре комнаты Kid. 0 комнаты совпадают (обычный случай).
* Влияет на kid_draw и метрики футпринта (fore) heal берёт запомненный
* прямоугольник, поэтому согласован автоматически. */
void pop_kid_set_render_dx(int dx);
/* Нарисовать текущий кадр Kid (банк GFX_BANK_SPRITE — видео-ОЗУ, без
* теневой копии; запоминает прямоугольник для kid_heal). */
void kid_draw(void);
/* Вернуть окно fore-клипа на прямоугольник последнего кадра Kid. Звать
* ПЕРЕД pop_fore_over_kid, если между ним и kid_draw кто-то ещё рисовал
* персонажа (pop_guard_draw ставит окно по стражу). */
void kid_fore_clip_restore(void);
/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid —
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
void kid_draw_splash(void);
/* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP
* (id 216 полное деление / 217 пустое). */
void pop_kid_img_blit(uint8_t image, int x, int y);
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
void kid_heal(void);
+455
View File
@@ -0,0 +1,455 @@
/*
* pop_level.c загрузка уровня PoP из сырого res200N.bin (Фаза L1).
* См. pop_level.h. Порт формата blueprnt DAT 1.0 (Table 6 POP-DAT).
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
#include "pop_level.h"
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
* замапленной в W0 страницей CPU прыгает на 0x0038 там обязан быть JP на
* _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но
* стабильный линкер тянет его из bgi256.lib (графика уже подключена). */
extern void _gfx_w0_isr(void);
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */
/* Эталонная копия foretable В ТОЙ ЖЕ странице: страница 16 КБ, данных
* уровня 2.3 КБ место даром. Нужна, потому что foretable теперь
* МУТАБЕЛЬНАЯ (pop_level_set_tile), а рестарт уровня в оригинале
* это load_level() заново (play_level, seg003:57), то есть все плиты целы
* и зелья на месте. pop_level_reset_tiles восстанавливает из неё. */
#define LVL_PRISTINE_OFF 0x1000u
/* Смещения блоков blueprnt относительно начала данных (Table 6). */
#define BP_FG 0u
#define BP_BG 720u
#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */
#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */
#define BP_LINKS 1952u
/* Стражи (level_type, types.h:228): по байту на комнату в каждом массиве. */
#define BP_GTILE 2119u
#define BP_GDIR 2143u
#define BP_GX 2167u
#define BP_GSEQLO 2191u
#define BP_GSKILL 2215u
#define BP_GSEQHI 2239u
#define BP_GCOLOR 2263u
#define BP_START 2112u
#define ROOM_TILES 30u
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
/* fwd: инициализация живой копии стражей (тело ниже). */
static void gstate_init(void);
static uint8_t lvl_blk, lvl_page, lvl_ok;
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
static uint8_t dl1[256], dl2[256];
int pop_level_load(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
lvl_blk = blk;
lvl_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(lvl_page);
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
uint16_t i;
stub = (uint16_t)&_gfx_w0_isr;
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
for (i = 0; i < 256; i++) {
dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
}
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
for (i = 0; i < 24u * ROOM_TILES; i++)
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
lvl_ok = 1;
gstate_init(); /* живая копия состояния стражей — из уровня */
return 0;
}
void pop_level_free(void)
{
if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; }
}
/* ---- Номер уровня и потабличные различия ---------------------------- */
uint8_t pop_current_level;
/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = демо).
* Держим целиком по 16 байт: короче и дешевле любых «оптимизаций», а
* расхождение с оригиналом сразу видно глазами. */
static const uint8_t tbl_entry_pose[16] =
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0 };
static const uint8_t tbl_guard_hp[16] =
{ 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0, 0 };
static const int8_t tbl_guard_type[16] =
{ 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1, -1 };
static const uint8_t tbl_level_type[16] =
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0 };
uint8_t pop_level_entry_pose(uint8_t n) { return n < 16 ? tbl_entry_pose[n] : 0; }
uint8_t pop_level_guard_hp(uint8_t n) { return n < 16 ? tbl_guard_hp[n] : 3; }
int8_t pop_level_guard_type(uint8_t n) { return n < 16 ? tbl_guard_type[n] : -1; }
uint8_t pop_level_tileset(uint8_t n) { return n < 16 ? tbl_level_type[n] : 0; }
int pop_level_load_num(uint8_t n)
{
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
* резидент W1 форматтер, которого здесь больше негде применить. */
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
uint8_t saved_blk = lvl_blk, saved_page = lvl_page, saved_ok = lvl_ok;
if (n > 15) return -1;
pri[12] = alt[8] = (char)('0' + n / 10);
pri[13] = alt[9] = (char)('0' + n % 10);
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
* pop_level_load сам аллоцирует страницу, и если файла нет остаёмся
* на текущем уровне вместо падения в пустой уровень. */
lvl_ok = 0; /* чтобы pop_level_load не мешался */
if (pop_level_load(pri) != 0 && pop_level_load(alt) != 0) {
lvl_blk = saved_blk; lvl_page = saved_page; lvl_ok = saved_ok;
return -1;
}
if (saved_ok) mem_free_block(saved_blk);
pop_current_level = n;
return 0;
}
/* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
* замаплена вызывающим). */
static const uint8_t *room_fg_ptr(uint8_t r)
{
return (const uint8_t *)(LVL_DATA_OFF + BP_FG) + (uint16_t)(r - 1) * ROOM_TILES;
}
static const uint8_t *room_bg_ptr(uint8_t r)
{
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
}
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg)
{
const uint8_t *rfg, *rbg, *lnk;
uint8_t left, right, down, i;
if (!lvl_ok || room < 1 || room > 24) return -1;
gfx_w0_map(lvl_page);
rfg = room_fg_ptr(room);
rbg = room_bg_ptr(room);
lnk = (const uint8_t *)(LVL_DATA_OFF + BP_LINKS) + (uint16_t)(room - 1) * 4;
for (i = 0; i < ROOM_TILES; i++) {
fg[i] = (uint8_t)(rfg[i] & 0x1F); /* тип тайла (верх. биты — модиф., пока не нужны) */
bg[i] = rbg[i]; /* backtable как есть */
}
left = lnk[0]; /* L */
right = lnk[1]; /* R */
down = lnk[3]; /* D */
/* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0),
* а в [3..5] ПРЕДпоследний (col8). Вторая колонка нужна коллизии: Kid,
* стоящий В шве (curr_col = 1), смотрит вперёд на колонку 2, и без неё
* там оказывалась мнимая стена (get_tile TILE_WALL), которая выпихивала
* его сквозь решётку. Оригинал резолвит любую колонку через
* find_room_of_tile (seg006:005D). */
if (left >= 1 && left <= 24) {
const uint8_t *lf = room_fg_ptr(left);
const uint8_t *lb = room_bg_ptr(left);
for (i = 0; i < 3; i++) {
lcol_fg[i] = (uint8_t)(lf[i * 10 + 9] & 0x1F);
lcol_fg[3 + i] = (uint8_t)(lf[i * 10 + 8] & 0x1F);
lcol_bg[i] = lb[i * 10 + 9];
}
} else {
for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_fg[3 + i] = 20; lcol_bg[i] = 0; }
}
/* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9:
* коллизия на правом шве + переход вправо) */
if (right >= 1 && right <= 24) {
const uint8_t *rf = room_fg_ptr(right);
const uint8_t *rb = room_bg_ptr(right);
for (i = 0; i < 3; i++) {
rcol_fg[i] = (uint8_t)(rf[i * 10 + 0] & 0x1F);
rcol_fg[3 + i] = (uint8_t)(rf[i * 10 + 1] & 0x1F); /* колонка 11 (см. выше) */
rcol_bg[i] = rb[i * 10 + 0];
}
} else {
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
}
/* belowrow: верхний ряд (row0) комнаты снизу — ОДИННАДЦАТЬ байт.
* [0..9] колонки 0..9 комнаты снизу; [10] тайл (0,9) комнаты
* ПО ДИАГОНАЛИ СНИЗУ-СЛЕВА (room_BL у оригинала). Одиннадцатый нужен
* тайлу (2,0): его «сосед снизу-слева» это колонка 1 ряда 3, то есть
* чужая комната, и load_rowbelow (seg008:368) резолвит её именно так,
* с дефолтом «стена» только когда такой комнаты НЕТ. Раньше мы клали
* туда стену БЕЗУСЛОВНО, и в пустом углу рисовался клин кладки
* (BUG-SEAM-WEDGE-1, ур. 3 комн. 18). */
if (down >= 1 && down <= 24) {
const uint8_t *df = room_fg_ptr(down);
uint8_t dl = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(down - 1) * 4];
for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F);
below_fg[10] = (dl >= 1 && dl <= 24)
? (uint8_t)(room_fg_ptr(dl)[9] & 0x1F)
: 20; /* кромка уровня */
} else {
for (i = 0; i < 11; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */
}
gfx_w0_unmap();
return 0;
}
int8_t pop_room_col_landing(uint8_t room, uint8_t col)
{
const uint8_t *rf;
int8_t row, res = -1;
if (!lvl_ok || room < 1 || room > 24 || col > 9) return -1;
gfx_w0_map(lvl_page);
rf = room_fg_ptr(room);
for (row = 0; row < 3; row++) { /* кусок пролетает пустые ряды */
uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
if (t == 0) continue; /* empty — дальше вниз */
if (t == 1) res = row; /* floor — приземление → debris */
break; /* первый не-пустой тайл — стоп */
}
gfx_w0_unmap();
return res;
}
/* set_tile-запись в ЖИВУЮ foretable уровня (порт curr_room_tiles[…] = …).
* В оригинале уровень обычная структура в ОЗУ, и do_pickup (seg006:1671),
* remove_loose (seg007:0EB8), loose_land (seg007:11E8) правят её насовсем.
* У нас уровень в EMM-странице пишем прямо туда, поэтому изменение
* переживает выход из комнаты (BUG-LVLSTATE-1). Пишем БАЙТ ЦЕЛИКОМ:
* старшие биты старого тайла (напр. 0x20 «solid loose») к новому типу
* отношения не имеют. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
{
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return;
gfx_w0_map(lvl_page);
((uint8_t *)(LVL_DATA_OFF + BP_FG))[(uint16_t)(room - 1) * ROOM_TILES + tilepos]
= tile;
gfx_w0_unmap();
}
/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть
* foretable в исходное состояние из эталонной копии. */
void pop_level_reset_tiles(void)
{
uint16_t i;
uint8_t *dst;
const uint8_t *src;
if (!lvl_ok) return;
gfx_w0_map(lvl_page);
dst = (uint8_t *)(LVL_DATA_OFF + BP_FG);
src = (const uint8_t *)LVL_PRISTINE_OFF;
for (i = 0; i < 24u * ROOM_TILES; i++) dst[i] = src[i];
gfx_w0_unmap();
}
/* Рестарт уровня, часть вторая (тот же load_level + pos_guards, seg003:83):
* стражи такая же часть данных уровня, как тайлы, и перезагрузка файла
* возвращает их всех. Убитый до смерти Кида страж после respawn снова жив
* и стоит на исходном тайле; забывается и запомненная поза трупа
* (guards_seq_hi = 0 -> «поднять стандартной стойкой»). */
void pop_level_reset_guards(void)
{
if (!lvl_ok) return;
gstate_init();
}
uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; }
uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; }
void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos)
{
uint8_t v;
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
gfx_w0_map(lvl_page);
v = (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
gfx_w0_unmap();
return v;
}
/* Батч-доступ: смаппить страницу уровня в W0 ОДИН раз, прочитать много
* тайлов через pop_level_tile_raw, размаппить один раз. Экономит N свапов
* окна 0 в горячих циклах (pop_process_trobs звал pop_level_tile на каждый
* trob каждый кадр). Между begin/end НЕЛЬЗЯ рисовать/звать код, которому
* нужно окно 0 в исходном состоянии только сырые чтения уровня. */
void pop_level_access_begin(void) { if (lvl_ok) gfx_w0_map(lvl_page); }
void pop_level_access_end(void) { if (lvl_ok) gfx_w0_unmap(); }
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
{
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
return (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
}
void pop_level_room_bg(uint8_t room, uint8_t *out30)
{
const uint8_t *rbg;
uint8_t i;
if (!out30) return;
if (!lvl_ok || room < 1 || room > 24) {
for (i = 0; i < ROOM_TILES; i++) out30[i] = 0;
return;
}
gfx_w0_map(lvl_page);
rbg = room_bg_ptr(room);
for (i = 0; i < ROOM_TILES; i++) out30[i] = rbg[i];
gfx_w0_unmap();
}
uint8_t pop_room_link(uint8_t room, uint8_t side)
{
uint8_t v;
if (!lvl_ok || room < 1 || room > 24 || side > 3) return 0;
gfx_w0_map(lvl_page);
v = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(uint16_t)(room - 1) * 4 + side];
gfx_w0_unmap();
return v;
}
static uint8_t start_byte(uint16_t off)
{
uint8_t v;
if (!lvl_ok) return 0;
gfx_w0_map(lvl_page);
v = ((const uint8_t *)(LVL_DATA_OFF))[off];
gfx_w0_unmap();
return v;
}
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
* колонки (pos_guards), поэтому guards_x из файла не читаем. */
/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ *
* В оригинале массивы стражей часть структуры уровня в ОЗУ, и движок их
* ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие
* позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq. Благодаря
* этому, вернувшись, находишь труп там же, а не воскресшего стража.
* У нас уровень лежит в EMM-странице только на чтение, поэтому держим
* мутабельную копию в W2: 6 байт на комнату. */
#define GS_TILE 0
#define GS_DIR 1
#define GS_X 2
#define GS_SKILL 3
#define GS_SEQL 4
#define GS_SEQH 5
static uint8_t gstate[24 * 6];
static void gstate_init(void)
{
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
uint8_t r;
gfx_w0_map(lvl_page);
for (r = 0; r < 24; r++) {
uint8_t *g = gstate + (uint16_t)r * 6;
g[GS_TILE] = p[BP_GTILE + r];
g[GS_DIR] = p[BP_GDIR + r];
g[GS_X] = p[BP_GX + r];
g[GS_SKILL] = p[BP_GSKILL + r];
/* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
* комнатах (оригинал чистит их в reset_level_unused_fields).
* Осмысленное значение туда кладёт только leave_guard, поэтому
* стартуем нулями: 0 = «поднимать стандартной стойкой».
* Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
g[GS_SEQL] = 0;
g[GS_SEQH] = 0;
}
gfx_w0_unmap();
}
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты. seq
* сохраняем ТОЛЬКО у мёртвого у живого seq_hi = 0 означает «поднять
* стандартной стойкой», как в оригинале. */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead)
{
uint8_t *g;
if (room < 1 || room > 24) return;
g = gstate + (uint16_t)(room - 1) * 6;
g[GS_TILE] = tile;
g[GS_DIR] = (uint8_t)dir;
g[GS_X] = x;
g[GS_SKILL] = skill;
if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
else { g[GS_SEQH] = 0; }
}
uint16_t pop_guard_state_seq(uint8_t room)
{
const uint8_t *g;
if (room < 1 || room > 24) return 0;
g = gstate + (uint16_t)(room - 1) * 6;
if (g[GS_SEQH] == 0) return 0; /* живой — стандартная стойка */
return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
}
/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
* центре тайла). */
uint8_t pop_guard_state_x(uint8_t room)
{
if (room < 1 || room > 24) return 0;
return gstate[(uint16_t)(room - 1) * 6 + GS_X];
}
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill)
{
const uint8_t *g;
uint8_t t;
if (!lvl_ok || room < 1 || room > 24) return 0;
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
g = gstate + (uint16_t)(room - 1) * 6;
t = g[GS_TILE];
if (t < 30) {
*tile = t;
*dir = (int8_t)g[GS_DIR];
*skill = g[GS_SKILL];
gfx_w0_map(lvl_page);
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
gfx_w0_unmap();
}
return (uint8_t)(t < 30);
}
uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); }
uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); }
int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); }
+142
View File
@@ -0,0 +1,142 @@
/*
* pop_level.h данные уровня PoP из сырого файла res200N.bin (Фаза L1).
*
* Уровень загружается ОДИН раз в отдельную EMM-страницу (как атласы:
* данные с offset 0x100, ISR-стаб в первых байтах страница безопасна
* для маппинга в W0). Файл сырой blueprnt DAT 1.0 (2305 байт, БЕЗ
* checksum-байта; Table 6 спецификации POP-DAT):
* foretable(fg) @0 backtable(bg) @720 door I/II @1440/1696
* links @1952 (24×{L,R,U,D}) start @2112 ...
* Тайл-код = байт fg & 0x1F (верхние биты модификатор, пока не нужны);
* bg передаётся как есть. См. applications/PoP/docs/POP-DAT-* .
*
* Рабочая копия ТЕКУЩЕЙ комнаты живёт в обычной памяти (W2) у приложения
* (мутабельная looseempty); pop_room_load извлекает её из страницы +
* тонкие срезы соседей для кромок (правый столбец left-комнаты, верхний
* ряд down-комнаты). Страница маппится в W0 только на время извлечения.
*/
#ifndef POP_LEVEL_H
#define POP_LEVEL_H
#include <stdint.h>
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
* (open/alloc/размер). Звать ПОСЛЕ первого atlas_load (pop_bg_load)
* он снимает страницу ядра DSS для W0-unmap. */
int pop_level_load(const char *path);
void pop_level_free(void);
/* ---- Номер уровня и потабличные различия (levels_plan.md §1) -------- */
/* Порт current_level (seg000 load_lev_spr: current_level = next_level =
* level). Уровень 1..15; 0 демо-уровень, в скоуп не входит. Читают
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
extern uint8_t pop_current_level;
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
* pop_current_level. 0 OK, -1 файла нет / не влез (тогда
* pop_current_level и страница НЕ тронуты играем дальше на старом).
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
int pop_level_load_num(uint8_t n);
/* tbl_entry_pose (data.h:848): 1 — падение внутрь (ур. 1), 2 — вбегание
* (ур. 13), 0 разворот на месте. Индекс номер уровня. */
uint8_t pop_level_entry_pose(uint8_t n);
/* tbl_guard_hp (data.h:846) — база HP стража на уровне (get_guard_hp,
* seg002:0044: extrastrength[skill] + tbl_guard_hp[level]). */
uint8_t pop_level_guard_hp(uint8_t n);
/* tbl_guard_type (data.h:844): 0 обычный, 1 толстый, 2 скелет, 3 визирь,
* 4 тень, 1 стражей на уровне НЕТ (уровни 14/15). */
int8_t pop_level_guard_type(uint8_t n);
/* tbl_level_type (data.h:840): 0 = dungeon, 1 = palace. Пока читается
* только для контроля второй тайлсет это levels_plan.md §3. */
uint8_t pop_level_tileset(uint8_t n);
/* Извлечь комнату room (1..24) в массивы приложения (W2):
* fg[30], bg[30] тайлы комнаты (fg маскирован &0x1F, bg raw);
* lcol_fg[6], lcol_bg[3] [0..2] правый столбец (col9) комнаты СЛЕВА
* (кромка col0), [3..5] предпоследний (col8)
* для коллизии на колонке 2 (Kid стоит В шве);
* rcol_fg[6] симметрично (col0 / col1);
* нет комнаты стена/0;
* below_fg[11] [0..9] верхний ряд (row0) комнаты СНИЗУ,
* [10] тайл (0,9) комнаты СНИЗУ-СЛЕВА («стена
* вниз»); нет комнаты стена.
* 0 OK, -1 уровень не загружен / room вне диапазона. */
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg);
/* Связь комнаты по стороне (0=L,1=R,2=U,3=D) → номер соседней (1..24) или
* 0, если стороны нет. Для переходов между комнатами (L2/L3). */
uint8_t pop_room_link(uint8_t room, uint8_t side);
/* Тайл-код (fg & 0x1F) по комнате (1..24) и позиции tilepos (0..29,
* row*10+col); вне диапазона 0 (empty). Для trob-диспетчера (pop_trob):
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
/* Записать тайл в ЖИВУЮ foretable уровня (порт curr_room_tiles[tp] = tile:
* do_pickup / remove_loose / loose_land). Изменение персистентно уровень
* лежит в ОЗУ-странице, а не в ПЗУ, то есть переживает выход из комнаты,
* как в оригинале. Отменяется только pop_level_reset_tiles. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile);
/* Рестарт уровня: вернуть ВСЕ тайлы в исходное (порт load_level, который
* play_level зовёт на каждой итерации в т.ч. после смерти Кида). */
void pop_level_reset_tiles(void);
/* Рестарт уровня: вернуть ВСЕХ стражей (тот же load_level + pos_guards).
* Убитый страж после respawn снова жив как в оригинале. */
void pop_level_reset_guards(void);
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
* Между begin/end только pop_level_tile_raw, без рисования. */
void pop_level_access_begin(void);
void pop_level_access_end(void);
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos);
/* Скопировать backtable (модификаторы) комнаты room в out[30]. Исходное
* состояние per-room modifier (openness ворот, состояние пик и т.п.) для
* ленивой инициализации room_modif в pop_trob. Вне диапазона нули. */
void pop_level_room_bg(uint8_t room, uint8_t *out30);
/* Таблицы связей дверей (LINKLOC@1440 / LINKMAP@1696, по 256 Б). Копируются
* в W2-RAM при загрузке (LINKMAP мутабельна таймеры кнопок). i индекс
* цепочки (модификатор кнопки). Декод в pop_trob (get_doorlink_*). */
uint8_t pop_doorlink1(uint8_t i);
uint8_t pop_doorlink2(uint8_t i);
void pop_doorlink2_set(uint8_t i, uint8_t v);
/* Ряд приземления падающего loose-куска в колонке col комнаты room: первый
* НЕ-пустой тайл сверху вниз; возвращает его ряд, если это floor ( debris),
* иначе -1 (пролетел/крушение о стену). */
int8_t pop_room_col_landing(uint8_t room, uint8_t col);
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
* Порт enter_guard (seg002:0112). */
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
* копии массивов уровня. dead != 0 сохранить и последовательность, тогда
* при возврате в комнату страж поднимется трупом там же, где лёг.
* tile == 30 (или больше) «стража в комнате больше нет». */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead);
/* Последовательность запомненного стража (0 — поднимать стандартной
* стойкой, как у живого). */
uint16_t pop_guard_state_seq(uint8_t room);
uint8_t pop_guard_state_x(uint8_t room);
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill);
uint8_t pop_level_start_room(void);
uint8_t pop_level_start_pos(void);
int8_t pop_level_start_dir(void);
#endif
File diff suppressed because it is too large Load Diff
+140 -14
View File
@@ -14,17 +14,130 @@
#include <stdint.h>
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). */
void pop_map_set(const uint8_t *fg);
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
* МУТАБЕЛЬНЫЙ (loose-пол при падении empty) передавать ту же копию,
* что и в pop_room_draw, чтобы отрисовка видела изменения. */
void pop_map_set(uint8_t *fg) __banked;
/* Установлен в 1, когда Kid упал ниже комнаты (комнаты снизу нет в этом
* тесте). Приложение должно сбросить Kid (kid_init) и снять флаг. */
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка 2)
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked;
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
* Звать в enter_room. */
void pop_map_set_room(uint8_t room) __banked;
/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт
* get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража
* (guards.c): он идёт по ряду между Кидом и стражем. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
/* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для
* pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */
void pop_determine_col(void) __banked;
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
* респавн). */
extern uint8_t pop_kid_dead;
extern uint8_t hitp_curr;
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
void pop_kid_hp_reset(void) __banked;
/* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт
* уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень
* подтягивает его к hitp_max (play_level_2, seg003:410) так «жизнь» из
* большой склянки переезжает на следующий уровень. Правит главный цикл. */
extern uint8_t hitp_beg_lev;
#define POP_START_HITP 3 /* custom->start_hitp */
#define POP_MAX_HITP 10 /* custom->max_hitp_allowed */
void pop_kid_hp_newgame(void) __banked; /* начало ИГРЫ: hitp_beg_lev = 3 */
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
* pop_have_sword «меч у Кида» (порт have_sword). pop_item_taken 0 или
* tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл
* (он стал полом) на ОБЕИХ страницах и записать override комнаты. */
extern uint8_t pop_have_sword;
extern uint8_t pop_item_taken;
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
extern uint8_t pop_flash_time;
/* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
extern uint8_t pop_flash_red;
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
* pop_room_link). Координаты Kid уже репроецированы (x140 / y+189).
* Приложение переключает комнату по соответствующей связи и снимает флаг. */
extern uint8_t pop_leave_dir;
/* exit_room_timer (порт seg002 exit_room): пока >0, боковой/нижний leave
* заблокирован (декремент в pop_phys_tick). Приложение ставит =2 при входе
* в комнату (enter_room) 2 кадра защиты от повторного/транзиентного leave
* (ping-pong у шва, транзиент отскока о ворота, баг #4/BUG-2). */
extern uint8_t pop_leave_timer;
/* Loose floors (проваливающиеся полы). pop_loose_modif[30] — состояние на
* тайл (0 покой / 0x80.. тряска / 1..11 отсчёт падения); читает pop_bg при
* отрисовке. pop_loose_tick() анимация + перерисовка изменившихся тайлов
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
* рядом с kid-отрисовкой. */
extern uint8_t pop_loose_modif[30];
void pop_loose_tick(void) __banked;
void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
extern uint8_t pop_loose_fell;
/* Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ (в оригинале — тот же loose с
* отрицательным tilepos, seg006 check_press frame 79 / seg007 animate_loose).
* pop_map_set_above даёт МУТАБЕЛЬНУЮ копию ряда 2 верхнего соседа (ту же, что
* ушла в pop_room_set_above): провалившаяся плита становится в ней EMPTY
* сверху открывается колодец. pop_ceil_modif[col] состояние (как
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
void pop_map_set_above(uint8_t *above_row2_fg) __banked;
extern uint8_t pop_ceil_modif[10];
extern uint8_t pop_ceil_fell;
extern uint8_t pop_debris_at;
/* Взводится в кадре, когда Kid получил урон (hitp_delta<0 оригинала) —
* приложение рисует «брызги» kid_draw_splash() поверх Kid и снимает флаг. */
extern uint8_t pop_kid_hurt;
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
* приземления/удара. Зовётся из pop_phys_tick. */
void pop_check_knock(void) __banked;
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
* комнаты снизу (y -= 189, curr_row = 0). Приложение решает: если есть
* links.down сменить комнату (pop_room_load+перерисовка) и продолжить;
* иначе выпал из уровня (респавн). Снять флаг после обработки. */
extern uint8_t pop_fell_out;
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
* determine_col + check_action (триггер падения / do_fall / приземление).
* Порядок как в оригинале (seg000 play_kid_frame). */
void pop_phys_tick(void);
void pop_phys_tick(void) __banked;
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
* над головой стена/пол, всё выше линии своей полосы не рисуется (Kid,
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
* obj_x ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y низ спрайта, w/h размер.
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
/* Парная половина clip_char: ЭКРАННЫЙ X, правее которого спрайт не рисуется
* (правый косяк двери уровня в кадрах подъёма 224..228). 0 = обрезки нет. */
int pop_clip_char_right(void) __banked;
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
@@ -33,26 +146,39 @@ void pop_phys_tick(void);
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
int pop_edge_distance(void);
uint8_t pop_edge_type(void);
int pop_edge_distance(void) __banked;
uint8_t pop_edge_type(void) __banked;
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
* позиции (передний край у стены, симметрично) и возвращает 1. Для
* control_running (чистый стоп у стены). */
int pop_wall_ahead(void);
int pop_wall_ahead(void) __banked;
/* Разбег-прыжок: выравнивание по кромке пола (run_jump, seg005:0AA8).
* Смотрит на 12 тайла вперёд; если там провал/пика подгоняет Kid.x под
* толчок ровно с кромки. 1 = прыгать, 0 = ОТМЕНИТЬ прыжок (Кид не в той
* фазе относительно кромки; control_up гасить НЕЛЬЗЯ попытка повторится
* на следующем кадре). Без этого провал в три тайла непроходим. */
uint8_t pop_run_jump_align(void) __banked;
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
* check_jump_up/grab_up (K4.4). control_up гасит caller. */
uint8_t pop_jump_up_seq(void);
uint8_t pop_jump_up_seq(void) __banked;
/* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
* pop_ctrl; здесь предикаты + выбор seq (+ выравнивание Kid.x). */
uint8_t pop_hang_grab_ready(void); /* grab_timer истёк — можно climb-up */
uint8_t pop_hang_climb_up_seq(void); /* seq_10 / seq_73 (подтягивание) */
int8_t pop_hang_wall(void); /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
uint8_t pop_hang_fall_seq(void); /* seq_23 / seq_11 (сорваться), +Kid.x */
uint8_t pop_down_action(void); /* 1=обработано(спуск/отступ), 0=присед */
uint8_t pop_hang_grab_ready(void) __banked; /* grab_timer истёк — можно climb-up */
uint8_t pop_hang_climb_up_seq(void) __banked; /* seq_10 / seq_73 (подтягивание) */
int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
/* Дверь уровня рядом и ОТКРЫТА ПОЛНОСТЬЮ? → поставить Кида в проём (x +
* разворот влево, go_up_leveldoor) и вернуть 1; иначе 0, Кид не тронут.
* Ветка двери уровня из up_pressed (seg005:0482). Последовательность
* seq_70 ставит вызывающий (pop_ctrl) тайлы наши, управление его. */
uint8_t pop_leveldoor_enter(void) __banked;
#endif
+79
View File
@@ -0,0 +1,79 @@
/*
* pop_redraw.c пометки перерисовки тайлов и их разбор. См. pop_redraw.h.
*
* Живёт в W1/W2 (НЕ в резиденте W3): пометки ставит логика, которая должна
* оставаться достижимой из будущих __banked-модулей. Сам разбор зовёт
* pop_bg (резидент W3) это законно, потому что разбор дёргает только
* главный цикл.
*/
#include <stdint.h>
#include "pop_redraw.h"
#include "pop_bg.h"
#define NTILES 30
#define NCOLS 10
static uint8_t rd_kind[NTILES], rd_cnt[NTILES];
static uint8_t rda_kind[NCOLS], rda_cnt[NCOLS];
/* Сколько всего непустых пометок: без него проход по 40 ячейкам крутился бы
* КАЖДЫЙ кадр впустую (замер 2026-07-27 на прежнем rest-цикле: ~12 % кадра). */
static uint8_t rd_pending;
void pop_redraw_reset(void)
{
uint8_t i;
for (i = 0; i < NTILES; i++) { rd_kind[i] = 0; rd_cnt[i] = 0; }
for (i = 0; i < NCOLS; i++) { rda_kind[i] = 0; rda_cnt[i] = 0; }
rd_pending = 0;
}
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
{
if (tilepos >= NTILES) return;
if (!rd_cnt[tilepos]) rd_pending++;
rd_kind[tilepos] = kind;
if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages;
}
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
{
if (col >= NCOLS) return;
if (!rda_cnt[col]) rd_pending++;
rda_kind[col] = kind;
if (pages > rda_cnt[col]) rda_cnt[col] = pages;
}
/* redraw_needed (seg008:0178): пройти пометки и нарисовать. row/col ведём
* бегущими счётчиками, а не через i/10 и i%10 у Z80 нет деления, и SDCC
* выносит __divuchar/__moduchar в вершину цикла как общее подвыражение
* аргументов (30*2 делений за кадр даже когда рисовать нечего). */
void pop_redraw_needed(void)
{
uint8_t i, row = 0, col = 0, left = 0;
if (!rd_pending) return;
for (i = 0; i < NTILES; i++) {
if (rd_cnt[i]) {
switch (rd_kind[i]) {
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break;
case POP_RD_FLOOR: pop_floor_bake(row, col); break;
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
case POP_RD_GATE: pop_gate_redraw(row, col); break;
default: break;
}
if (--rd_cnt[i]) left++;
else rd_kind[i] = 0;
}
if (++col == 10) { col = 0; row++; }
}
for (i = 0; i < NCOLS; i++) {
if (rda_cnt[i]) {
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
else pop_ceil_bake_empty(i);
if (--rda_cnt[i]) left++;
else rda_kind[i] = 0;
}
}
rd_pending = left;
}
+51
View File
@@ -0,0 +1,51 @@
/*
* pop_redraw.h пометки «перерисовать тайл» и их разбор.
*
* Порт архитектуры оригинала: логика анимации НИЧЕГО не рисует, она ставит
* флаг (`set_redraw_full`/`set_wipe`/`redraw_20h`/`redraw_21h`, seg007), а
* отрисовка идёт отдельным проходом (`redraw_needed`, seg008:0178). У нас
* так же: pop_map/pop_trob зовут pop_set_redraw*, а pop_redraw_needed()
* (главный цикл) разбирает пометки и дёргает pop_bg.
*
* ЗАЧЕМ (кроме сходства с SDLPoP): из __banked-кода резидентная страница W3
* недостижима ни напрямую, ни транзитивно (docs/layout_plan_v2.md §2 R2).
* Пока pop_map звал pop_bg, его нельзя было использовать из банка стражей.
* Пометки живут в W1/W2 и зовутся откуда угодно.
*
* ОТЛИЧИЕ ОТ ОРИГИНАЛА: у нас не один экран с dirty-rect, а две страницы
* дабл-буфера со своей ОЗУ-копией фона, поэтому счётчик пометки это ЧИСЛО
* СТРАНИЦ (обычно 2), а вид перерисовки хранится явно: heal+рисовать поверх
* (транзиент) или запечь новый фон. В оригинале вид кодируется тем, в какой
* из таблиц redraw_frames_* стоит флаг.
*/
#ifndef POP_REDRAW_H
#define POP_REDRAW_H
#include <stdint.h>
/* Виды перерисовки тайла (row*10+col). */
#define POP_RD_SPIKE 1 /* пики: heal + оба тайла (redraw_21h) */
#define POP_RD_LOOSE 2 /* loose: дрожащий кадр / кадр покоя */
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */
/* Виды перерисовки полосы у потолка (по колонке). */
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
#define POP_RDA_CEIL_GONE 2 /* плита-потолок провалилась: колодец */
/* Пометить тайл/колонку: kind — вид, pages — на скольких страницах
* дабл-буфера перерисовать (1 = только текущая, 2 = обе). Повторная
* пометка перекрывает вид и берёт максимум счётчика. */
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages);
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages);
/* Разобрать пометки: нарисовать всё помеченное на ТЕКУЩЕЙ (back) странице и
* уменьшить счётчики. Звать из главного цикла в слое ФОНА до kid_draw. */
void pop_redraw_needed(void);
/* Сбросить все пометки (смена комнаты/уровня). */
void pop_redraw_reset(void);
#endif
+60
View File
@@ -0,0 +1,60 @@
/*
* pop_state.c данные, которые читают ОБЕ стороны банковой границы.
*
* Зачем отдельный модуль: sprinter-cc собирает банковый модуль с
* --dataseg BANKn, поэтому ВСЯ его писучая память (глобалы, file-static,
* статики внутри функций) линкуется В СТРАНИЦУ БАНКА и снаружи не читается
* проверено на .map (2026-07-30: глобал банкового модуля лёг по
* 0x0001C000, а не в _DATA). Значит любую переменную, к которой нужен
* доступ и из банка, и снаружи, надо держать в НЕбанковом модуле здесь.
*
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
*/
#include "pop_state.h"
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
uint8_t pop_loose_landed;
/* ---- Шина управления (seg006 globals) ------------------------------ *
* Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка
* pop_ctrl (банк 5) пишет с клавиатуры, guards.c (банк 1) подаёт
* синтетический ввод ИИ (autocontrol_*, seg002), pop_map (банк 3) читает
* через pop_ctrl_shift_held. Сегодня оно уцелело бы и в pop_ctrl.c
* (собираем без --bank-data, данные банков остаются в _DATA), но это
* флаг сборки: включат его и шина уедет в страницу банка 5.
*
* control_x/y/shift «сырое» состояние осей ЭТОГО кадра (абсолютно:
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 конечный
* автомат held/released/ignore (авто-повтор). */
int8_t control_x, control_y, control_shift;
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
* но HP не теряет иначе проверять боёвку приходится с трёх попаданий. */
uint8_t pop_immortal;
/* Габариты последней нарисованной двери уровня (порт leveldoor_right /
* leveldoor_ybottom): пишет draw_leveldoor из банка pop_bg, читает
* clip_char из pop_map. См. pop_state.h. */
int pop_leveldoor_right;
int pop_leveldoor_ybottom;
/* ---- Отладочная «пустышка» для брейкпоинтов из БАНКОВ ---------------- *
* Зачем. PC-брейкпоинт видит только логический адрес, а 0xC000+ это
* окно, куда мапятся ВСЕ банки: точка на адресе банковой функции ловит
* заодно чужой код, случайно легший по тому же смещению (проверено:
* точка на start_fall из банка 3 срабатывала на каждом кадре попадала
* в pop_bg из банка 2). Условные брейкпоинты этот отладчик MAME не
* поддерживает («error in assignment expression» на `==`).
*
* Приём (идея пользователя, 2026-08-04): позвать ЭТУ функцию ровно из
* того места банкового кода, которое отлаживаем, и поставить брейкпоинт
* на неё она резидентна в W1, её адрес однозначен. По возврату из неё
* читаем что нужно. Условие «когда именно ловить» пишется обычным `if`
* в C это гибче любых выражений отладчика.
*
* Счётчик нужен, чтобы вызов не выкинул оптимизатор; заодно видно, сколько
* раз точка прошла, если ловим не останавливаясь. */
uint8_t pop_dbg_hits;
void pop_dbg_trap(void) { pop_dbg_hits++; }
+38
View File
@@ -0,0 +1,38 @@
/*
* pop_state.h состояние, общее для банков и W1/W2. См. pop_state.c.
*
* Правило: если переменную читают/пишут по обе стороны банковой границы
* её место здесь, а НЕ в банковом модуле (там она уедет в страницу банка).
*/
#ifndef POP_STATE_H
#define POP_STATE_H
#include <stdint.h>
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
* tilepos+1 тайла, на который он лёг. Ставит отрисовка mob (банк),
* разбирает логика loose-полов (pop_map, W1/W2). */
extern uint8_t pop_loose_landed;
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
extern uint8_t pop_immortal;
/* Габариты ПОСЛЕДНЕЙ нарисованной двери уровня — порт глобалов
* leveldoor_right / leveldoor_ybottom (seg008 draw_leveldoor). Их ставит
* отрисовка (банк pop_bg), а читает clip_char (pop_map, W1/W2): кадры
* подъёма по лестнице (224..228) обрезаются по правому косяку проёма и по
* низу поднятой створки. Дверь рисуется ПОД персонажем, поэтому перекрыть
* его не может оригинал именно режет спрайт.
* right экранный X правой кромки; ybottom room-space Y (без POP_YOFF).
* 0 = двери в отрисованной комнате нет. */
extern int pop_leveldoor_right;
extern int pop_leveldoor_ybottom;
/* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c.
* Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на
* _pop_dbg_trap (резидент W1, адрес однозначен). */
extern uint8_t pop_dbg_hits;
void pop_dbg_trap(void);
#endif
+541
View File
@@ -0,0 +1,541 @@
/*
* pop_trob.c P0-каркас: room_modif + список trob + диспетчер анимаций.
* Порт SDLPoP seg007 (process_trobs/animate_tile/add_trob/animate_spike).
* См. pop_trob.h.
*/
#include "pop_trob.h"
#include "pop_level.h"
#include "pop_geom.h"
#include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */
/* pop_bg нужен ТОЛЬКО для пламени факела и пузырька зелья: это не тайловая
* перерисовка, а покадровый оверлей поверх фона (в оригинале свои
* add_backtable/add_foretable внутри draw_tile). Всё остальное идёт через
* пометки, поэтому pop_process_trobs единственная функция модуля, которую
* нельзя звать из __banked-кода. */
#include "pop_bg.h"
#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */
#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */
/* Коды тайлов (fg & 0x1F), которые анимируются. */
#define TILE_SPIKE 0x02
#define TILE_GATE 0x04
#define TILE_CLOSER 0x06
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
#define TILE_OPENER 0x0F
#define TILE_POTION 0x0A
#define TILE_LOOSE 0x0B /* проваливающийся пол (фаза тряски в modif) */
#define TILE_SWORD 0x16 /* меч-предмет на полу */
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
#define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E
/* ---- Персистентное per-room modifier-состояние -------------------- */
static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES];
static uint8_t room_seen[POP_NROOMS]; /* modif проинициализирован из уровня */
/* ---- Список анимируемых тайлов ------------------------------------ */
typedef struct { uint8_t room, tilepos; int8_t type; } trob_t;
#define TROBS_MAX 30 /* как в оригинале (types.h) */
static trob_t trobs[TROBS_MAX];
static uint8_t trobs_count;
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
* признак «нажата» = таймер связи >1). Редрой пики/кнопки ТОЛЬКО при её
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
* стоящим Кидом перерисовывалась КАЖДЫЙ кадр (2×draw_tile 50% кадра в
* статике), т.к. её trob висит, пока Кид стоит (это верно, как в SDLPoP
* расхождение было именно в безусловном редрое). */
static uint8_t trob_drawn[POP_ROOMTILES];
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
* выбора следующего кадра пламени. */
static pop_rnd_t trob_seed;
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
void pop_trob_reset(void) __banked
{
uint8_t i;
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
pop_redraw_reset();
trobs_count = 0;
}
uint8_t *pop_trob_modif(uint8_t room) __banked
{
if (room < 1 || room > POP_NROOMS) return 0;
if (!room_seen[room - 1]) {
uint8_t i;
pop_level_room_bg(room, room_modif[room - 1]);
/* load_alter_mod (seg008:198E): модификатор В ФАЙЛЕ уровня и
* модификатор В РАНТАЙМЕ разные величины, оригинал переводит их
* при загрузке. Порт того, что нас касается:
*
* ворота 1 = «открыты» (Table 8 спецификации DAT) -> 188, то есть
* рабочая высота подъёма; ЛЮБОЕ другое значение -> 0
* (закрыты). Без этого перевода ворота, которые в
* уровне открыты, стартуют закрытыми, и кнопка-closer
* рядом с ними теряет смысл (ур. 2, комн. 13).
* loose -> 0: фаза тряски всегда начинается с нуля.
* зелье -> <<3: старшие биты = тип, младшие 3 = фаза пузырька.
*
* Ветка стен (blue-line/связи кладки) НЕ портируется намеренно: у
* нас pop_bg считает связи по типам СОСЕДЕЙ прямо при отрисовке
* (wall_modifier), сохранённый модификатор стены не читается вовсе. */
pop_level_access_begin();
for (i = 0; i < POP_ROOMTILES; i++) {
uint8_t *m = &room_modif[room - 1][i];
switch (pop_level_tile_raw(room, i) & 0x1F) {
case TILE_GATE: *m = (uint8_t)((*m == 1) ? 188 : 0); break;
case TILE_LOOSE: *m = 0; break;
case TILE_POTION: *m = (uint8_t)(*m << 3); break;
default: break;
}
}
pop_level_access_end();
room_seen[room - 1] = 1;
}
return room_modif[room - 1];
}
static int find_trob(uint8_t room, uint8_t tilepos)
{
uint8_t i;
for (i = 0; i < trobs_count; i++)
if (trobs[i].room == room && trobs[i].tilepos == tilepos) return i;
return -1;
}
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
{
int f = find_trob(room, tilepos);
if (f >= 0) { trobs[f].type = type; return; }
if (trobs_count >= TROBS_MAX) return;
trobs[trobs_count].room = room;
trobs[trobs_count].tilepos = tilepos;
trobs[trobs_count].type = type;
trobs_count++;
}
void pop_trob_room_changed(void) __banked
{
uint8_t i;
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
pop_redraw_reset();
}
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
* тайлов: факелам случайная стартовая фаза пламени (start_anim_torch,
* seg007:0847: modif = prandom(8)), зельям случайная фаза пузырька
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
void pop_trob_anim_room(uint8_t room) __banked
{
uint8_t *mod = pop_trob_modif(room);
uint8_t i, code;
if (!mod) return;
pop_level_access_begin();
/* Освободить список от «декоративных» trob ДРУГИХ комнат: факел, зелье и
* блеск меча анимируются только в ОТРИСОВАННОЙ комнате, а место в списке
* занимают. Оригинал их копит и на 30-й выдаёт «Trobs Overflow»; у нас
* отладочный обход всех 24 комнат упирается в лимит за пару минут, после
* чего новые анимации молча не заводятся. Механику (ворота/пики/кнопки,
* в т.ч. кросс-комнатные) чистка не трогает. */
{
uint8_t k, wr = 0;
for (k = 0; k < trobs_count; k++) {
uint8_t c = (uint8_t)(pop_level_tile_raw(trobs[k].room,
trobs[k].tilepos) & 0x1F);
if (trobs[k].room != room &&
(c == TILE_TORCH || c == TILE_TORCH_DEBRIS ||
c == TILE_POTION || c == TILE_SWORD))
continue;
trobs[wr++] = trobs[k];
}
trobs_count = wr;
}
for (i = 0; i < POP_ROOMTILES; i++) {
code = (uint8_t)(pop_level_tile_raw(room, i) & 0x1F);
if (code == TILE_TORCH || code == TILE_TORCH_DEBRIS) {
mod[i] = (uint8_t)trob_prandom(8);
pop_add_trob(room, i, 1);
} else if (code == TILE_POTION) {
mod[i] = (uint8_t)((mod[i] & 0xF8) | (trob_prandom(6) + 1));
pop_add_trob(room, i, 1);
} else if (code == TILE_SWORD) {
/* start_anim_sword (seg007:087C): случайная фаза счётчика блеска. */
mod[i] = (uint8_t)(trob_prandom(0xFF) & 0x1F);
pop_add_trob(room, i, 1);
}
}
pop_level_access_end();
}
/* animate_spike (seg007:04D3): автомат по modifier. *m — modifier тайла,
* *type тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4),
* на 5 0x8F (полностью вышли, безвредны), на 9 0 + конец. Убирание
* (флаг 0x80): --modif; когда 0x7F-часть обнулилась modif=6 (начать
* визуальный уход 6..8). 0xFF = disabled. */
/* get_torch_frame (seg007:06CD): случайный кадр 0..8, иначе следующий. */
static uint8_t get_torch_frame(uint8_t curr)
{
uint8_t next = (uint8_t)trob_prandom(255);
if (next != curr && next < 9) return next;
next = (uint8_t)(curr + 1);
if (next >= 9) next = 0;
return next;
}
/* bubble_next_frame (seg007:0759): фазы пузырька 1..7 по кругу. */
static uint8_t bubble_next_frame(uint8_t curr)
{
uint8_t next = (uint8_t)(curr + 1);
return (next >= 8) ? 1 : next;
}
static void animate_spike(uint8_t *m, int8_t *type)
{
uint8_t mod = *m;
if (*type >= 0) {
if (mod == 0xFF) return; /* disabled — не двигать */
if (mod & 0x80) {
--mod;
if (mod & 0x7F) { *m = mod; return; }
mod = 6;
} else {
++mod;
if (mod == 5) mod = 0x8F;
else if (mod == 9) { mod = 0; *type = -1; }
}
}
*m = mod;
}
/* ---- Кнопки/ворота (seg007) --------------------------------------- *
* Таблицы связей LINKLOC/LINKMAP в pop_level (мутабельная копия в W2).
* doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */
static uint8_t get_doorlink_timer(uint8_t i) { return (uint8_t)(pop_doorlink2(i) & 0x1F); }
static void set_doorlink_timer(uint8_t i, uint8_t v)
{
pop_doorlink2_set(i, (uint8_t)((pop_doorlink2(i) & 0xE0) | (v & 0x1F)));
}
static uint8_t get_doorlink_tile(uint8_t i) { return (uint8_t)(pop_doorlink1(i) & 0x1F); }
static uint8_t get_doorlink_next(uint8_t i) { return (uint8_t)(!(pop_doorlink1(i) & 0x80)); }
static uint8_t get_doorlink_room(uint8_t i)
{
return (uint8_t)(((pop_doorlink1(i) & 0x60) >> 5) + ((pop_doorlink2(i) & 0xE0) >> 3));
}
/* trigger_gate (seg007:092C): изменить openness ворот по типу кнопки.
* Возвращает тип анимации для add_trob (или -1 = ничего). */
static int8_t trigger_gate(uint8_t room, uint8_t tilepos, uint8_t button_type)
{
uint8_t *mod = pop_trob_modif(room);
uint8_t modifier = mod[tilepos];
if (button_type == TILE_OPENER) {
if (modifier == 0xFF) return -1; /* уже насовсем открыт */
if (modifier >= 188) { mod[tilepos] = 238; return -1; } /* открыт → держать */
mod[tilepos] = (uint8_t)((modifier + 3) & 0xFC);
return 1; /* обычное открытие */
} else if (button_type == TILE_DEBRIS) { /* насовсем (died_on_button) */
if (modifier < 188) return 2;
mod[tilepos] = 0xFF;
return -1;
} else { /* closer */
if (modifier != 0) return 3; /* быстро закрыть */
return -1;
}
}
/* trigger_1 (seg007:0999): цель = ворота → trigger_gate; иначе -1 (leveldoor
* и прочее отложено). */
static int8_t trigger_1(uint8_t target_type, uint8_t room, uint8_t tilepos, uint8_t button_type)
{
if (target_type == TILE_GATE)
return trigger_gate(room, tilepos, button_type);
if (target_type == TILE_LEVELDOOR) {
/* trigger_1 (seg007:0999): дверь уровня открывается ОДИН раз — если
* створка уже тронулась (modif != 0), кнопка ничего не делает. */
uint8_t *m = pop_trob_modif(room);
if (m && m[tilepos] == 0) return 1;
return -1;
}
return -1;
}
/* do_trigger_list (seg007:09E5): обойти цепочку LINKLOC от index, для каждой
* цели добавить trob (если trigger_1 >= 0). */
static void do_trigger_list(uint8_t index, uint8_t button_type)
{
for (;;) {
uint8_t room = get_doorlink_room(index);
uint8_t tilepos = get_doorlink_tile(index);
uint8_t target_type = pop_level_tile(room, tilepos);
int8_t result = trigger_1(target_type, room, tilepos, button_type);
if (result >= 0) pop_add_trob(room, tilepos, result);
if (get_doorlink_next(index) == 0) break;
index++;
}
}
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier) __banked
{
uint8_t link_timer = get_doorlink_timer(modifier);
if (link_timer != 0x1F) { /* не заклинено */
/* Оригинал ставит 5; вынесено в pop_tune.h, потому что проверять
* руками проход в закрывающуюся решётку при таком окне почти
* невозможно (см. BUG-GATE-PASS-1). */
set_doorlink_timer(modifier, POP_DBG_GATE_HOLD);
if (link_timer < 2) {
pop_add_trob(room, tilepos, 1); /* кнопка нажимается */
/* seg007:766: щелчок кнопки — шум, страж оборачивается. Звука у
* нас нет, флаг обязан быть (BUG-GUARD-DEAF-1). */
is_guard_notice = 1;
}
do_trigger_list(modifier, button_type);
}
}
/* animate_button (seg007:0D3A): отсчитать таймер связи; <2 → кнопка
* отжимается (trob кончился). idx = модификатор кнопки (индекс LINKLOC). */
static void animate_button(uint8_t idx, int8_t *type)
{
if (*type >= 0) {
uint8_t timer = get_doorlink_timer(idx);
timer = (uint8_t)(timer ? timer - 1 : 0);
set_doorlink_timer(idx, timer);
if (timer < 2) *type = -1;
}
}
/* animate_leveldoor (seg007:05F1): створка двери уровня. type 0..2 —
* открытие (modif 0 -> 43, по пикселю за кадр), type >= 3 быстрое
* закрытие со скоростями {0,5,17,99} (после выхода из уровня). Готово ->
* type = -1. */
static const uint8_t leveldoor_close_speeds[5] = {0, 5, 17, 99, 0};
static void animate_leveldoor(uint8_t *m, int8_t *type)
{
if (*type < 0) return;
if (*type >= 3) {
uint8_t sp;
if (*type < 7) (*type)++;
sp = leveldoor_close_speeds[*type - 3];
if (sp >= *m) { *m = 0; *type = -1; }
else *m = (uint8_t)(*m - sp);
} else {
if (*m < 43) (*m)++;
if (*m >= 43) { *m = 43; *type = -1; }
}
}
/* animate_door (seg007:0522): openness по типу анимации. *m — modif ворот,
* *type 0 закрытие / 1 открытие / 2 насовсем / 3..8 быстрое закрытие. */
static const uint8_t gate_close_speeds[9] = {0, 0, 0, 20, 40, 60, 80, 100, 120};
static const int8_t door_delta[3] = {-1, 4, 4};
static void animate_door(uint8_t *m, int8_t *type)
{
int8_t anim_type = *type;
uint8_t mod = *m;
if (anim_type >= 0) {
if (anim_type >= 3) { /* быстрое закрытие */
if (anim_type < 8) { anim_type++; *type = anim_type; }
{
int new_mod = (int)mod - gate_close_speeds[anim_type];
if (new_mod < 0) { mod = 0; *type = -1; }
else mod = (uint8_t)new_mod;
}
} else if (mod != 0xFF) {
mod = (uint8_t)(mod + door_delta[anim_type]);
if (anim_type == 0) { /* закрытие */
if (mod == 0) *type = -1; /* gate_stop */
} else { /* открытие */
if (mod >= 188) {
if (anim_type < 2) { mod = 238; *type = 0; } /* открыт → потом закрывать */
else { mod = 0xFF; *type = -1; } /* насовсем */
}
}
} else {
*type = -1;
}
}
*m = mod;
}
uint8_t pop_trob_room_active(uint8_t room) __banked
{
uint8_t i;
for (i = 0; i < trobs_count; i++)
if (trobs[i].room == room) return 1;
return 0;
}
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 3
* vsync и соответствует игровому тику оригинала):
* 1 кадр пламени каждый тик, как в оригинале; кадры при этом ровные по
* тактам, что нужно для счёта бюджета;
* 2 вдвое реже (так было до 2026-07-30; пламя шло на половинной
* скорости, а бюджет кадра «прыгал» на 6.2 % от того, попал кадр на
* анимацию или нет). */
#define TORCH_ANIM_DIV 1
#if TORCH_ANIM_DIV > 1
static uint8_t torch_tick;
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
#else
#define TORCH_ANIM_NOW 1
#endif
void pop_process_trobs(uint8_t cur_room) __banked
{
uint8_t i, w = 0;
#if TORCH_ANIM_DIV > 1
torch_tick++;
#endif
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
* подвыражение аргументов redraw 30*2=60 делений/кадр даже когда нет
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
if (trobs_count == 0) return;
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
* pop_level_tile() на каждый trob (тот свапал W0 каждый вызов = N свапов
* за кадр даже без анимации). Дальше цикл (с редроями) идёт с
* размапленным W0 как раньше. */
{
uint8_t k;
pop_level_access_begin();
for (k = 0; k < trobs_count; k++)
trob_code[k] = pop_level_tile_raw(trobs[k].room, trobs[k].tilepos);
pop_level_access_end();
}
for (i = 0; i < trobs_count; i++) {
uint8_t room = trobs[i].room;
uint8_t tp = trobs[i].tilepos;
uint8_t code = trob_code[i];
uint8_t *mod = pop_trob_modif(room);
int8_t type = trobs[i].type;
switch (code) {
case TILE_SPIKE:
animate_spike(&mod[tp], &type);
break;
case TILE_OPENER:
case TILE_CLOSER:
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
break;
case TILE_GATE:
animate_door(&mod[tp], &type);
break;
case TILE_LEVELDOOR:
animate_leveldoor(&mod[tp], &type);
break;
case TILE_TORCH:
case TILE_TORCH_DEBRIS:
/* animate_torch (seg007:03C1): анимация вечная — type остаётся
* 1, пока комната отрисована. Темп TORCH_ANIM_DIV. */
if (room == cur_room && TORCH_ANIM_NOW)
mod[tp] = get_torch_frame(mod[tp]);
break;
case TILE_POTION:
/* animate_potion (seg007:03E9): крутим младшие 3 бита (фаза
* пузырька), старшие 5 тип зелья. */
if (room == cur_room)
mod[tp] = (uint8_t)((mod[tp] & 0xF8) | bubble_next_frame(mod[tp] & 7));
break;
case TILE_SWORD:
/* animate_sword (seg007:0425): счётчик вниз; на 0 — новый период
* 0x28..0x67. Блеск рисуется РОВНО на modif==1 (draw_tile берёт
* кадр (modif==1)+10), т.е. одиночная вспышка раз в 40..103 тика. */
if (room == cur_room) {
if (--mod[tp] == 0)
mod[tp] = (uint8_t)((trob_prandom(255) & 0x3F) + 0x28);
}
break;
default:
type = -1;
break;
}
trobs[i].type = type;
if (room == cur_room &&
(code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) {
/* Пламя/пузырёк анимируются КАЖДЫЙ кадр и в фон не запечены —
* рисуем поверх (сами делают heal своей области). */
uint8_t trow = tp / 10, tcol = tp % 10;
if (code == TILE_POTION) pop_potion_draw(trow, tcol, mod[tp]);
else pop_torch_draw(trow, tcol, mod[tp]);
}
if (room == cur_room && code == TILE_SPIKE) {
/* ПИКИ — редрой КАЖДЫЙ кадр, пока trob жив: animate_spike
* (seg007:0353) зовёт redraw_21h БЕЗУСЛОВНО, а не по смене кадра.
* Это не «лишняя работа оригинала»: выдвинутая пика живёт только в
* видео-ОЗУ (банк SPRITE, в фон не запечена), а kid_heal каждый кадр
* возвращает под спрайтом Кида ПЕЧЁНЫЙ фон то есть стирает те
* части остриёв, что попали под его прямоугольник. При редрое
* по смене кадра пика держится 15 кадров (modif 0x8F..0x81) с
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
* странице дабл-буфера из двух). */
/* Последний кадр (пика убралась, trob умирает) — пометить ОБЕ
* страницы: живой trob метит каждый кадр по одной. */
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
}
if (room == cur_room && code == TILE_GATE) {
/* draw_trob (seg007:01E6), которым заканчивается animate_door:
* пока trob жив решётка едет каждый кадр, помечаем ТЕКУЩУЮ
* страницу; последний кадр (type<0, ворота встали) ОБЕ, иначе
* на второй странице дабл-буфера застынет предыдущее положение
* и решётка дрожит через кадр. Тайлпос САМИ ворота, соседа
* справа выбирает pop_gate_redraw. */
pop_set_redraw(tp, POP_RD_GATE, (uint8_t)(type < 0 ? 2 : 1));
}
if (room == cur_room && code == TILE_LEVELDOOR) {
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
* kid_heal вернул бы из фона закрытую створку. Куски двери
* непрозрачны, поэтому wipe не нужен. */
/* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
* помечаем на обе страницы, иначе дверь дрожит через кадр. */
if (tp % 10 + 1 < 10)
pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR,
(uint8_t)(type < 0 ? 2 : 1));
}
if (room == cur_room && code == TILE_SWORD) {
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
* спрайт запечён в фон (draw_tile), поэтому перерисовываем тайл
* той же схемой, что и кнопку текущая страница сразу, вторая
* следующим кадром через rest-цикл. */
uint8_t sig = (uint8_t)(mod[tp] == 1);
if (sig != trob_drawn[tp]) {
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
trob_drawn[tp] = sig;
}
}
if (room == cur_room && (code == TILE_OPENER || code == TILE_CLOSER)) {
/* КНОПКА — редрой только при смене видимого состояния (нажата =
* таймер связи >1): пока Кид её держит, картинка стабильна.
* Рисуем ТЕКУЩУЮ страницу сразу + <rest>=1 (вторая страница
* дабл-буфера следующим кадром через rest-цикл). tp/10,tp%10
* считаем только в момент редроя (редко), а не каждый кадр. */
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
if (sig != trob_drawn[tp]) {
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
trob_drawn[tp] = sig;
}
}
}
/* compact: удалить завершённые (type < 0). */
for (i = 0; i < trobs_count; i++)
if (trobs[i].type >= 0) trobs[w++] = trobs[i];
trobs_count = w;
}
+65
View File
@@ -0,0 +1,65 @@
/*
* pop_trob.h P0-каркас: персистентное per-room modifier-состояние +
* список анимируемых тайлов (trob). Порт SDLPoP seg007 (process_trobs/
* add_trob/animate_tile) + curr_room_modif.
*
* ЗАЧЕМ: тайлы-объекты (пики, ворота, кнопки) имеют состояние (modifier),
* которое живёт МЕЖДУ входами в комнату и может меняться КРОСС-КОМНАТНО
* (кнопка в одной комнате открывает ворота в другой). room_modif[room]
* единственный источник модификаторов для отрисовки (pop_bg draw_tile
* читает его как bg), как curr_room_modif в оригинале.
*
* Список trob хранит (room, tilepos, type) активных анимаций; каждый кадр
* pop_process_trobs продвигает их (animate_spike/door/button по коду тайла),
* пишет результат в room_modif и перерисовывает изменившиеся тайлы ТЕКУЩЕЙ
* комнаты на back-странице (2-page, как loose). Невидимые комнаты
* анимируются в состоянии (без отрисовки) при входе рисуются статически.
*/
#ifndef POP_TROB_H
#define POP_TROB_H
#include <stdint.h>
#define POP_NROOMS 24
#define POP_ROOMTILES 30
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
* очистить список trob. */
void pop_trob_reset(void) __banked;
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
* (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне
* диапазона 0. */
uint8_t *pop_trob_modif(uint8_t room) __banked;
/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить
* type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked;
/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать
* modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице.
* Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */
void pop_process_trobs(uint8_t cur_room) __banked;
/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку
* LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button.
* room/tilepos кнопка; button_type её тайл-код; modifier её modif
* (= индекс в LINKLOC). Звать из check_press (pop_map). */
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier) __banked;
/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через
* шов (roomtest не дёргает шов, пока сосед не анимируется). */
uint8_t pop_trob_room_active(uint8_t room) __banked;
/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты
* (trob-состояние в room_modif персистентно и НЕ трогается). Звать в
* enter_room. */
void pop_trob_room_changed(void) __banked;
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
* зелья) со случайной стартовой фазой. Звать в enter_room. */
void pop_trob_anim_room(uint8_t room) __banked;
#endif
+66
View File
@@ -0,0 +1,66 @@
/*
* pop_tune.h НАСТРАИВАЕМЫЕ КОНСТАНТЫ движка (аналог custom_options
* SDLPoP) в одном месте.
*
* Зачем отдельный заголовок. У оригинала «особые события» уровней зашиты
* магическими числами прямо в код (`current_level == 3 && Char.room == 7`),
* и SDLPoP вынес их в структуру `custom_options_type` (types.h) с
* загрузкой из SDLPoP.ini и правкой через меню Settings/Mods. Нам нужно
* то же самое по двум причинам:
*
* 1. ОТЛАДКА. Некоторые сцены руками почти не проверить (успеть в
* закрывающуюся решётку), и единственный вменяемый способ временно
* растянуть тайминг. Такие крутилки обязаны жить в одном месте, а не
* прятаться числами по трём модулям.
* 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа.
*
* Пока это только `#define` ни INI, ни меню нет (см. TASKS.md, запись
* TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг
* заводится ЗДЕСЬ, а не константой по месту. Имена как у SDLPoP
* (custom->имя), чтобы сверка с оригиналом оставалась механической.
*/
#ifndef POP_TUNE_H
#define POP_TUNE_H
/* ---- Чекпойнт уровня 3 (SDLPoP: checkpoint_*) ----------------------- *
* Оригинал: level3_set_chkp (seg002:0665) взводит флаг, когда Кид уходит
* ВЛЕВО ИЗ комнаты 7 (вызов стоит до goto_other_room, поэтому Char.room там
* ещё старая); do_startpos (seg003:0141) подменяет по
* нему стартовую позицию и убирает loose-плиту, через которую туда
* попадают (иначе после респавна дорога назад была бы уже провалена). */
#define POP_CHKP_LEVEL 3 /* checkpoint_level */
#define POP_CHKP_ROOM 7 /* комната, ИЗ КОТОРОЙ уходят влево (у
* SDLPoP хардкод; это НЕ комната
* назначения см. roomtest.c) */
#define POP_CHKP_RESPAWN_ROOM 2 /* checkpoint_respawn_room */
#define POP_CHKP_RESPAWN_TILEPOS 6 /* checkpoint_respawn_tilepos */
#define POP_CHKP_RESPAWN_DIR (-1) /* checkpoint_respawn_dir: влево */
#define POP_CHKP_CLEAR_ROOM 7 /* checkpoint_clear_tile_room */
#define POP_CHKP_CLEAR_COL 4 /* checkpoint_clear_tile_col */
#define POP_CHKP_CLEAR_ROW 0 /* checkpoint_clear_tile_row */
/* ---- Отладочные крутилки (в оригинале и в SDLPoP их НЕТ) ------------ *
* POP_DBG_GATE_HOLD сколько кадров решётка держится поднятой после
* нажатия кнопки, ПРЕЖДЕ чем пойдёт вниз. В оригинале это таймер связи
* дверей: trigger_button ставит 5, animate_button вычитает по единице, и
* решётка стоит, пока таймер > 1 (то есть ~4 кадра после того, как Кид
* сошёл с кнопки). Проверять руками проход в закрывающуюся решётку при
* таком окне почти невозможно, поэтому значение вынесено сюда: поставь
* 30 и успеешь дойти спокойно. ОБЯЗАН быть 5 в игровой сборке.
*
* ПОТОЛОК 30. Таймер связи пять бит модификатора кнопки (маска 0x1F), а
* значение 31 у оригинала означает «событие заклинено» (`link_timer != 0x1F`
* в trigger_button). Больше 30 ставить нельзя кнопка перестанет
* срабатывать вовсе. */
#define POP_DBG_GATE_HOLD 5
/* ---- Улучшения SDLPoP (в ОРИГИНАЛЕ их нет) -------------------------- *
* POP_ENABLE_JUMP_GRAB зацепиться за кромку можно ПРЯМО В ПРЫЖКЕ, если
* Shift зажат заранее (SDLPoP `enable_jump_grab`, seg006:1228). Ваниль
* разрешает зацеп только в начале падения (`check_grab`, кадры 102..105),
* то есть Shift приходится нажимать уже в полёте это и раздражало на
* приёмке. 0 как в оригинале, 1 как в SDLPoP с включёнными
* enhancements. */
#define POP_ENABLE_JUMP_GRAB 1
#endif /* POP_TUNE_H */
-10
View File
@@ -1,10 +0,0 @@
/* room1_data.h — данные комнаты 1 ур.1 (res2001) + левый столбец
* соседа (room 5 col9) для границы-решётки. Сгенерировано
* gen из render_room; fg=foretable code, bg=backtable modifier. */
#ifndef ROOM1_DATA_H
#define ROOM1_DATA_H
static const unsigned char room1_fg[30] = {0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x14,0x14,0x13,0x13,0x01,0x03,0x00,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x0E,0x03,0x0B,0x01,0x01,0x14};
static const unsigned char room1_bg[30] = {0xFF,0xFF,0xFF,0x00,0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00};
static const unsigned char leftcol_fg[3] = {0x04,0x14,0x14};
static const unsigned char leftcol_bg[3] = {0x01,0x00,0x00};
#endif
+749 -18
View File
@@ -13,30 +13,471 @@
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "room1_data.h"
#include "pop_level.h"
#include "pop_trob.h"
#include "pop_redraw.h"
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
#include "pop_guard.h"
#include "pop_cheat.h"
#include "pop_tune.h"
__sfr __at (0xFE) io_border;
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
* выключить). Цвет бордюра держится, пока фаза считает, поэтому высота
* полосы на скриншоте = время фазы в растровых строках. Это единственный
* способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
* отвечает из фреймового колбэка, все сэмплы падают в обработчик
* прерывания); точные такты breakpoint'ы с {printf totalcycles; g}.
*
* ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
* бит 1 (синий) и бит 2 (зелёный) значит различимых состояний ровно
* ЧЕТЫРЕ, и значения обязаны быть чётными. Отсюда фазы сгруппированы по
* четыре, а не по семь (нечётные номера раньше сливались с соседями и
* полосы не читались):
* 0 чёрный ожидание vsync (запас кадра)
* 2 синий ввод, heal и ЛОГИКА (kid_tick/физика/страж)
* 4 зелёный слой ФОНА (loose/trobs/redraw_needed/шов)
* 6 циан СПРАЙТЫ (страж, Kid, fore поверх них) */
#ifndef PROF_BORDER
#define PROF_BORDER 1
#endif
#if PROF_BORDER
#define PROF(n) io_border = (n)
#else
#define PROF(n) ((void)0)
#endif
/* Сколько банков грузит crt0_banked (читает его стартовый код).
* 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP),
* 5 = pop_ctrl (диспетчер управления).
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
* вплотную (16 021 из 16 384), а расти ему ещё есть куда тайлы поздних
* уровней, чомперы, зеркало.
*
* ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть это не ошибка
* сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */
const uint8_t n_banks = 6;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
*
* При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и
* одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание тоже
* 5). Приёмный FIFO SIO 3 байта, а импульс запроса прерывания здесь
* теряется примерно в 44 % случаев, поэтому байты копятся и один из
* полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
*
* Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько
* вызовов за кадр они попадают туда, где прерывания и так разрешены, и
* лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
* без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя машина
* уходит в перезагрузку.
*
* Что помогает: опрос раз в ~0.5 мс. Столько времени берётся из ожидания
* кадра процессор всё равно крутит там ~42 мс из 60, ничего не делая.
* Поэтому kbd_raw_poll вешается idle-хуком на gfx_wait_vsync (см.
* kbd_idle ниже); полезной работы это не отнимает. Потолок приёма
* подтверждён отдельно (tests/kbdpoll: 25 нажатий 25, 250 байт из 250). */
/* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает
* «было ли что вычерпано». Приводить указатель кастом не хочется. */
static void kbd_idle(void)
{
kbd_raw_poll();
}
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2
#define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
#define FIRST_LEVEL 1 /* с какого уровня начинается игра */
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
* ряда) следующая/предыдущая комната уровня ПО НОМЕРУ (1..24, с обёрткой),
* Kid ставится на первый попавшийся пол. Нужен, чтобы пройти все комнаты и
* собрать список багов отрисовки, включая недостижимые обычным путём.
* Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */
static uint8_t room_fg[30], room_bg[30];
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
* смотрит наружу (колонка 2 / 11). См. pop_level.h. */
static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11];
static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid
* слева от отрисованной 140 (спрайт за левой кромкой), справа +140, та
* же 0. Звать каждый кадр перед kid_draw. */
static void update_kid_render_dx(void)
{
int dx = 0;
if (kid_room != cur_room) {
if (kid_room == pop_room_link(cur_room, 0)) dx = -140; /* left neighbour */
else if (kid_room == pop_room_link(cur_room, 1)) dx = 140; /* right neighbour */
}
pop_kid_set_render_dx(dx);
}
/* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) -----------------------
*
* В оригинале никто не «поверх» по определению: и Кид, и страж попадают в
* midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а
* тайлы обходятся в фиксированном порядке redraw_needed_tiles (seg008:1B06)
* идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ тот
* поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или
* выше, его тайл в обходе позже. Совпали тайлы решает sort_curr_objs
* (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже.
*
* Мы рисовали стражКид всегда, то есть Кид был поверх безусловно.
*
* Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 (2 - row). */
#define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col))
/* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */
static int8_t tile_div_mod(int x)
{
int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */
int c = v / TILE_SIZEX;
if (v % TILE_SIZEX < 0) --c;
return (int8_t)c;
}
/* char_x_left из set_char_collision (seg006:0723) — левая граница габарита.
* ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к
* *_coll, а колонки char_col_left/right считает по этой. */
static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx,
uint8_t flags, uint16_t w)
{
int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */
uint8_t fl = flags;
if (dir < 0) fl ^= 0x80;
/* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */
if (!(fl & 0x80) && e <= 57) e++;
if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */
return e;
}
/* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа.
*
* Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил
* сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ
* колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому
* бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров
* бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих:
* enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт
* дал бы перекос в другую сторону. */
static void objtile_at_char(int8_t *row, int8_t *col,
const pop_char_t *ch, int8_t dx, int8_t dy,
uint8_t flags, uint16_t w)
{
int8_t r = ch->curr_row, c = ch->curr_col;
if (ch->action == 1) { /* actions_1_run_jump */
int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy));
if (br == -1) br = 3; /* char_bottom_row */
r = br;
c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w));
if (c < 0) c = 0; /* MAX(char_col_left, 0) */
}
if ((ch->frame >= 135 && ch->frame < 149) ||
ch->action == 2 || ch->action == 3 ||
ch->action == 4 || ch->action == 6) c--;
*row = r; *col = c;
}
/* 1 = страж рисуется ПОСЛЕ Кида (поверх). */
static uint8_t guard_over_kid(void)
{
int8_t kr, kc, gr, gc;
int kk, gk;
if (Guard.charid == 0 || Guard.room != cur_room) return 0;
objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(),
kid_cur_flags(), kid_fp_width());
objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy,
pop_gframe.flags, pop_guard_fp_width());
kk = OBJ_ORDER_KEY(kr, kc);
gk = OBJ_ORDER_KEY(gr, gc);
if (gk != kk) return (uint8_t)(gk > kk);
/* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */
return (uint8_t)((int)Guard.y + pop_gframe.dy > kid_fp_obj_y());
}
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
* реально изменился. Пока Кид держит кнопку и гейт открыт modifier стабилен
* 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен
* тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера. */
static uint8_t seam_sig; /* последняя нарисованная openness гейта в шве */
static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */
/* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая):
* sig = последняя показанная комната, redraw = остаток кадров (2 = обе страницы). */
static uint8_t room_label_sig = 0xFF;
static uint8_t room_label_redraw;
/* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления
* куска, поднятый предмет пол) пишутся ПРЯМО В УРОВЕНЬ
* pop_level_set_tile, как в оригинале (curr_room_tiles[] = ). Раньше тут
* жила таблица переопределений на 8 записей, и девятая правка молча
* терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
* комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS 0x0E
#define TILE_EMPTY 0x00
#define TILE_FLOOR 0x01
/* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
* flash_on вспышка фона активна (нулевая запись палитры подменена). */
static uint8_t item_pos, item_bake, flash_on;
/* Подмена НУЛЕВОЙ записи палитры на обеих страницах (do_flash/set_bg_attr).
* Через функцию с параметрами, а не пятью литералами в месте вызова: на
* gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
* (см. .sprinter-cc-roomtest/roomtest.asm) и ассемблер падает. */
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
{
gfx_pal_set(0, 0, r, g, b);
gfx_pal_set(1, 0, r, g, b);
}
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
* активной страницу 1 вызывающий восстанавливает нужную draw-страницу. */
static void enter_room(uint8_t room)
{
uint8_t pg;
pop_guard_leave(); /* запомнить стража уходящей комнаты */
uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
pop_map_set(room_fg);
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
pop_room_link(room, 2), pop_room_link(room, 3),
lcol_fg, rcol_fg);
pop_room_set_below(below_fg);
/* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика
* уровня, mod живой room_modif (ленивая инициализация). */
{
uint8_t up = pop_room_link(room, 2), c;
if (up) {
uint8_t *um = pop_trob_modif(up);
pop_level_access_begin();
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
pop_level_access_end();
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
pop_room_set_above(above_fg, above_mod);
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
} else {
pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */
pop_map_set_above(0);
}
}
pop_map_set_room(room);
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
* (Kid.room отрисованной у шва) ещё не портирована, но сравнение
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
* стража никогда не срабатывает. */
Kid.room = room;
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
* а не по свежему backtable. Фаза P0. */
room_mod = pop_trob_modif(room);
/* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот,
* видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6).
* pop_room_load заполнил lcol_bg/rcol_bg статикой уровня перекрываем. */
{
uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1);
if (lft) { uint8_t *m = pop_trob_modif(lft);
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
seam_sig = m[9]; } /* шов рисуется в обе страницы ниже — синхр. сигнатуру */
if (rgt) { uint8_t *m = pop_trob_modif(rgt);
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; }
}
seam_redraw = 0; /* шов свежий на обеих страницах — редрой не нужен */
for (pg = 0; pg < 2; pg++) {
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
}
cur_room = room;
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
}
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
* (seg003:028A) поставить Кида в стартовую позицию УРОВНЯ. Один вход и
* для запуска, и для рестарта после смерти / выпадения из уровня: в
* оригинале это буквально один и тот же путь (play_level_2 зовёт
* start_level заново), поэтому и у нас не три копии четырёх строк.
*
* Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define:
* start_pos тайл 0..29 (row*10+col), start_dir направление, в котором
* персонаж должен ОКАЗАТЬСЯ.
*
* Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж
* ставится спиной и доворачивается кадрами позы входа. Сама поза из
* tbl_entry_pose (data.h:848 = {0,1,0,,2,}):
* 1 (уровень 1) падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2);
* это оно захлопывает решётку за спиной в первой комнате;
* 2 (уровень 13) вбегание (seq_84_run);
* иначе разворот на месте (seq_5_turn).
* Таблица индексируется НОМЕРОМ УРОВНЯ pop_level_entry_pose(). */
#define SEQ_TURN 5
#define SEQ_FALL 7
#define SEQ_RUN 84
#define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */
#define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */
#define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */
/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ —
* это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью
* (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72).
* Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки:
* его стартовый тайл (комната 5, ряд 1, колонка 3) правая половина
* той самой двери. */
static void find_start_level_door(uint8_t room)
{
uint8_t *m = pop_trob_modif(room);
uint8_t tp;
if (!m) return;
/* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз
* за уровень, а pop_add_trob сам лезет в W0. */
for (tp = 0; tp < 30; tp++) {
if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue;
m[tp] = 43; /* створка поднята полностью */
pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */
}
}
/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7
* уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет
* стартовую позицию. Сбрасывается началом игры и прохождением уровня
* ровно как в оригинале (seg003:37 и seg003:413). */
static uint8_t pop_checkpoint;
static void pop_start_level(void)
{
uint8_t entry_pose = pop_level_entry_pose(pop_current_level);
uint8_t start_room = pop_level_start_room();
uint8_t pos = pop_level_start_pos();
int8_t dir = (int8_t)~pop_level_start_dir();
uint8_t seq = SEQ_TURN;
/* Особое событие: старт с чекпойнта (do_startpos, seg003:0141).
* Числа в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles()
* ниже, иначе рестарт её вернёт. */
uint8_t chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint);
if (chkp) {
start_room = POP_CHKP_RESPAWN_ROOM;
pos = POP_CHKP_RESPAWN_TILEPOS;
dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную
* и разворачивается seq_5 */
}
/* Рестарт уровня = load_level() заново (play_level, seg003:57): в
* исходное возвращаются И модификаторы (пики/ворота), И сами тайлы
* разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и
* убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */
pop_trob_reset();
pop_level_reset_tiles();
pop_level_reset_guards();
pop_guard_reset(); /* start_level: Guard.direction = none */
pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */
/* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в
* комнату-чекпойнт после респавна остался бы непройденным. */
if (chkp)
pop_level_set_tile(POP_CHKP_CLEAR_ROOM,
(uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL),
TILE_EMPTY);
/* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos
* отрабатывает раньше первого draw_room), иначе шов кадр показывал бы
* ещё не тронутое состояние решётки. */
if (entry_pose == ENTRY_POSE_FALL) {
/* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5,
* col2, row0 её же типом и модификатором (в оригинале и то и другое
* берётся из curr_* после get_tile). */
uint8_t tp = 2; /* row0*10 + col2 */
pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]);
seq = SEQ_FALL;
} else if (entry_pose == ENTRY_POSE_RUN) {
seq = SEQ_RUN;
}
/* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть
* открыта уже в первом кадре (в оригинале find_start_level_door стоит
* между do_startpos и draw_level_first). */
find_start_level_door(start_room);
enter_room(start_room);
kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir);
Kid.sword = 0; /* set_start_pos: sword_0_sheathed */
/* play_level (seg003:106): have_sword = level >= have_sword_from_level
* (=2) начиная со второго уровня меч у Кида уже есть. */
pop_have_sword = (uint8_t)(pop_current_level >= 2);
pop_kid_hp_reset();
}
int main(void)
{
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
* по умолчанию (без мерцания); SPACE переключает в однобуфер для
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
uint8_t pg, front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint16_t dead_frames = 0;
#ifdef ROOMNAV
uint8_t nav_prev = 0;
#endif
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;
uint8_t xdec_prev = 0, xinc_prev = 0;
if (pop_bg_load() != 0) { /* атласы — ДО initgraph */
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
puts("pop_bg_load failed");
return 1;
}
pop_cheats = 1; /* режим разработки: читы включены */
pop_guard_reset();
if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
printf("kid_data.bin load failed\n");
return 1;
}
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed");
return 1;
}
if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */
puts("level load failed");
return 1;
}
pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */
pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
pop_checkpoint = 0; /* init_game (seg003:37) */
initgraph();
gfx_set_visible_page(0);
if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */
if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */
gfx_pal_fload(0, "a:\\kid.pal");
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
@@ -44,18 +485,13 @@ int main(void)
* ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
* страницы, в которую рисуем. Дальше каждый тик восстанавливаем
* ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
for (pg = 0; pg < 2; pg++) {
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
pop_room_draw(1, room1_fg, room1_bg, leftcol_fg, leftcol_bg);
}
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
/* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
* pop_start_level. Управление: / бег, присед, + прыжок с места,
* Shift шаг, ESC выход. Карта коллизии задана внутри enter_room. */
pop_start_level();
gfx_set_draw_page(0);
/* K2b: Kid стоит на уступе (row1), управляется с клавиатуры.
* / бег, присед, + прыжок с места, Shift шаг (K3). ESC выход. */
pop_map_set(room1_fg); /* карта для коллизии/падения (K3) */
kid_init(SEQ_STAND, 1, 1, 0);
kid_draw();
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(),
@@ -66,10 +502,14 @@ int main(void)
puts("kbd_raw open failed");
return 1;
}
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
while (!kbd_raw_down(KBD_ESC)) {
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */
PROF(2); /* ввод + heal */
uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) {
dbuf ^= 1;
@@ -85,32 +525,319 @@ int main(void)
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0;
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K убить стража;
* I бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза);
* S выдать меч, не бегая за ним в комнату 15. Штатно меч
* берётся с пола (pop_have_sword ставит подбор);
* Shift+L следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет;
* [ / ] подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
* персонажа в этом порте задаёт не диспетчер (грабли
* L1-EXIT). Колонку не трогаем её пересчитает физика
* следующего кадра. */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
cheat_prev = k;
immo_prev = i;
sword_prev = s;
nlvl_prev = nl;
xdec_prev = xd;
xinc_prev = xi;
}
if (frozen) {
PROF(0); /* чёрный: ждём vsync */
gfx_wait_vsync();
continue;
}
#ifdef ROOMNAV
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
* '-' (0x7B / 0x4E) назад. Edge по nav_prev: одно нажатие = одна
* комната.
*
* Здесь БЫЛ pop_trob_reset() перед enter_room «чтобы каждая комната
* смотрелась в исходном состоянии». Убран: reset обнуляет room_seen,
* и модификаторы перечитываются из уровня заново, то есть чит
* ОТМАТЫВАЕТ МИР открытые/закрытые ворота, выдвинутые пики, нажатые
* кнопки. Пока ворота с bg=1 ошибочно стартовали закрытыми, откат был
* не виден; после порта load_alter_mod он выглядит как баг «ворота
* снова открылись» (уровень 1, решётка комнаты 5 (0,9), которую
* закрывает стартовый триггер do_startpos). Навигация обязана только
* телепортировать; исходное состояние даёт перезапуск уровня. */
{
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
if (nav && !nav_prev) {
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
: (uint8_t)(r == 1 ? 24 : r - 1);
enter_room(r);
/* Предпочитаем обычный пол; иначе — любой tile_is_floor
* (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
for (i = 0; i < 30; i++) {
c = (uint8_t)(room_fg[i] & 0x1F);
if (c == 1) { any = i; break; }
if (any == 0xFF && c && c != 9 && c != 12 && c != 20 &&
!(c >= 26 && c <= 29)) any = i;
}
if (any != 0xFF) pos = any;
kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0);
pop_kid_hp_reset();
dead_frames = 0;
}
nav_prev = nav;
}
#endif
/* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0. */
back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back);
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
pop_guard_heal(); /* и прошлый кадр стража */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика (тот же синий) */
kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */
if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести HP. */
pop_check_sword_hurting();
pop_check_sword_hurt();
pop_do_delta_hp();
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0;
kid_init(SEQ_STAND, 1, 1, 0);
if (down) { /* L2: переход в комнату снизу — продолжает падать */
enter_room(down); /* координаты Kid уже репроецированы pop_map */
gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */
} else { /* выпал из уровня (нет комнаты снизу) — рестарт */
pop_start_level();
gfx_set_draw_page(back);
}
kid_draw(); /* нарисовать новый кадр */
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
uint8_t left = (uint8_t)(pop_leave_dir == 1);
pop_leave_dir = 0;
/* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал
* зовёт его из leave_room ДО goto_other_room, то есть Char.room
* там ещё СТАРАЯ. Значит условие `Char.room == 7` читается как
* «Кид УХОДИТ ИЗ комнаты 7 влево» (в комнату 2), а не «входит в
* неё». Смысл: чекпойнт даётся за то, что комната 7 пройдена
* НАСКВОЗЬ, и респавн ставится туда, куда Кид только что попал.
*
* Сначала я поставил проверку на комнату НАЗНАЧЕНИЯ и это
* поймал пользователь прогоном в SDLPoP: он умер В комнате 7, но
* влево из неё не выходил, и оригинал вернул его в стартовую
* комнату 9, а плита осталась цела. Заодно тут запоминается
* достигнутый потолок HP: смерть после чекпойнта его не отбирает. */
if (left && pop_current_level == POP_CHKP_LEVEL &&
cur_room == POP_CHKP_ROOM) {
pop_checkpoint = 1;
hitp_beg_lev = hitp_max;
}
if (nb) {
enter_room(nb);
gfx_set_draw_page(back);
}
}
PROF(4); /* слой фона */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
{ /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) {
uint8_t *m = pop_trob_modif(lft);
uint8_t g = m[9], prev = seam_sig;
seam_sig = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */
/* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */
/* не g — sdcc_z80_cmp_store_a_bug) */
if (g != prev) seam_redraw = 2; /* openness изменился → обе страницы дабл-буфера */
if (seam_redraw) {
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
pop_room_redraw_seam_left();
seam_redraw--;
}
}
}
if (pop_next_level != pop_current_level) {
/* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81):
* цикл уровня кончается, когда next_level разошёлся с
* current_level (END_LEVEL после seq_70 в двери уровня либо чит
* Shift+L), и play_level грузит следующий.
*
* HP переносится: hitp_beg_lev = hitp_max ДО загрузки так
* поднятая на уровне «жизнь» (большая склянка) переезжает
* дальше, а урон нет (do_startpos дольёт до потолка). */
hitp_beg_lev = hitp_max;
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
if (pop_level_load_num(pop_next_level) != 0) {
pop_next_level = pop_current_level; /* файла нет — остаёмся */
} else {
/* Всё состояние прошлого уровня сносит сам pop_start_level:
* pop_trob_reset обнуляет room_seen (модификаторы комнат
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
* из неё же, loose чистит enter_room. */
pop_start_level();
}
gfx_set_draw_page(back);
}
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++;
if (dead_frames > RESPAWN_SETTLE &&
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
dead_frames = 0;
pop_start_level();
gfx_set_draw_page(back);
}
}
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0;
pop_level_set_tile(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) {
int8_t lrow = pop_room_col_landing(down, fcol);
if (lrow >= 0) pop_level_set_tile(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
}
}
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
uint8_t up = pop_room_link(cur_room, 2);
pop_ceil_fell = 0;
if (up) pop_level_set_tile(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
}
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
pop_level_set_tile(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
pop_debris_at = 0;
}
if (pop_item_taken) { /* предмет поднят → тайл стал полом */
item_pos = (uint8_t)(pop_item_taken - 1);
item_bake = 2; /* запечь на ОБЕИХ страницах */
pop_level_set_tile(cur_room, item_pos, TILE_FLOOR);
pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
pop_item_taken = 0;
}
if (item_bake) { /* стереть спрайт меча из фона страницы */
pop_floor_bake(item_pos / 10, item_pos % 10);
item_bake--;
}
/* Вспышка фона — порт flash_if_hurt/remove_flash_if_hurt (seg003:0AFC):
* do_flash(color) = set_bg_attr(0, color), т.е. подмена НУЛЕВОЙ записи
* палитры (вспыхивает всё чёрное поле). Оригинал ставит цвет и СНИМАЕТ
* его В ТОМ ЖЕ кадре, поэтому пока идёт flash_time видно СТРОБ
* «жёлтый/чёрный», а не одну длинную заливку. Снятие после первого
* gfx_wait_vsync (ниже): цвет держится ~1 растровый кадр из трёх.
* Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) {
/* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color
* оригинала; нам хватает этих двух). */
if (pop_flash_red) flash_bg(255, 0, 0); /* color_4_red */
else flash_bg(255, 255, 85); /* color_14_brightyellow */
pop_flash_time--;
if (!pop_flash_time) pop_flash_red = 0;
flash_on = 1;
} else if (pop_kid_hurt) {
flash_bg(255, 85, 85); /* color_12_brightred */
flash_on = 1;
}
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
* персонажа (см. guard_over_kid). */
uint8_t g_after = guard_over_kid();
if (!g_after) pop_guard_draw();
PROF(6); /* kid_draw (тот же циан) */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
if (g_after) pop_guard_draw();
}
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash();
pop_kid_hurt = 0;
}
pop_hp_draw(); /* полоса HP — только при изменении */
PROF(6); /* fore поверх Kid + борта */
/* Окно fore-клипа общее, и pop_guard_draw ставит своё. При «страж
* поверх» он отрабатывает ПОСЛЕ kid_draw, поэтому окно Кида
* восстанавливаем явно иначе передние грани у Кида отсекаются и
* он рисуется поверх столбов. */
kid_fore_clip_restore();
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action);
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже
* сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */
PROF(0); /* чёрный: ждём vsync */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
#ifdef ROOMNAV
{
uint8_t r = cur_room, prev = room_label_sig;
room_label_sig = r; /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */
if (r != prev) room_label_redraw = 2;
if (room_label_redraw) {
/* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный
* знакогенератор (_gfx_font_buf, 2 КБ статики в W2) и сажает
* кучу с 3867 до 576 Б. Слева десятки, справа единицы:
* || |||| = 24. bar/setfillstyle уже используются. */
uint8_t i;
setfillstyle(SOLID_FILL, BLACK);
bar(100, 0, 175, 5);
/* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная
* (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */
setfillstyle(SOLID_FILL, 0x57);
for (i = 0; i < (uint8_t)(r / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5);
for (i = 0; i < (uint8_t)(r % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5);
room_label_redraw--;
}
}
#endif
gfx_wait_vsync();
if (flash_on) { /* строб: цвет держался один кадр растра */
flash_bg(0, 0, 0);
flash_on = 0;
}
gfx_wait_vsync();
gfx_wait_vsync();
if (dbuf) {
@@ -119,9 +846,13 @@ int main(void)
}
}
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
pop_ctrl_close();
closegraph();
pop_bg_free();
pop_kid_free();
pop_guard_free();
pop_kid_data_free();
pop_level_free();
return 0;
}
@@ -0,0 +1 @@
build/
@@ -0,0 +1,52 @@
# Модульные тесты движка roomtest под ucsim_z80. Обвязка общая (testkit/),
# здесь — только сами наборы и список модулей, которые в них линкуются.
TESTKIT := $(abspath $(CURDIR)/../../../../testkit)
ENGINE_DIR := $(abspath $(CURDIR)/..)
# t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
# абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88).
CODE_LOC := 0x1000
# Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/
# sprinter: под тестом эти функции заглушены (stubs.c), но объявления
# берутся настоящие — иначе разъедутся сигнатуры.
PROJ_ROOT := $(abspath $(TESTKIT)/..)
EXTRA_INC := -I$(PROJ_ROOT)/libc/include -I$(PROJ_ROOT)/libbgi/include -I$(CURDIR)
OBJS_geom := build/eng_pop_geom.rel
OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
build/eng_pop_map.rel build/eng_pop_ctrl.rel \
build/eng_pop_state.rel \
build/stubs.rel build/scene.rel build/kid_blob.rel \
build/bank_stub.rel
# Пробный набор (разведка, без эталонов) линкуется тем же составом.
OBJS_grab := $(OBJS_phys)
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
OBJS_wall := $(OBJS_phys)
include $(TESTKIT)/host-tests.mk
# --- t_phys: блоб таблиц анимации ------------------------------------
KID_BIN := $(abspath $(CURDIR)/../../poc/res/kid/kid_data.bin)
build/kid_blob.c: gen_kid_blob.py $(KID_BIN) | $(BUILD)
@python3 gen_kid_blob.py $(KID_BIN) $@
build/kid_blob.rel: build/kid_blob.c kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/stubs.rel: stubs.c stubs.h kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/scene.rel: scene.c scene.h stubs.h kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/bank_stub.rel: bank_stub.s | $(BUILD)
@$(SDASZ80) -plosgff $@ $<
# Эталоны трасс: пересобрать набор, если их правили (см. шапку t_phys.c).
build/phys.rel: $(wildcard golden/*.h)
@@ -0,0 +1,105 @@
# tests-host — модульные тесты движка roomtest
Прогоняются под `ucsim_z80`, обвязка общая — `testkit/` (там же объяснение,
почему под z80, а не хостовым gcc, и грабли).
```
make host-tests # из корня, вместе с остальными
make -C applications/PoP/roomtest/tests-host # только эти
make -C applications/PoP/roomtest/tests-host t_geom
```
## Что покрыто
| набор | модуль | что проверяет |
|-------|--------|---------------|
| `t_geom` | `pop_geom.c` | геометрия комнаты (`x_bump`, `y_land`, `y_to_row`) и PRNG оригинала |
| `t_phys` | `pop_map.c` + `pop_kid.c` + `pop_ctrl.c` | покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал |
Главный тест в `t_geom``geom_lcg_matches_reference`. LCG оригинала
(`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме
Горнера ради обхода `__mullong`, и заявка «бит-в-бит как в SDLPoP» до
появления теста держалась только на комментарии. Тест сверяет рукописный
asm с наивной 32-битной формулой на 128 шагах — и по возвращаемому
значению, и по обеим половинам сида.
## t_phys: характеризация, а не проверка правильности
Набор существует ради **страховки под рефакторинг**. Эталоны сняты с
текущего билда, то есть консервируют и текущие баги — правильность
по-прежнему проверяется сверкой с `SDLPoP/src/`. Ценность в другом:
ближайший крупный шаг (перенос физики с `Kid.` на `Char.`, чтобы её мог
переиспользовать страж) обязан быть **поведение-сохраняющим**, и «трасса до
== трасса после» ловит ровно тот класс ошибок, который там возможен.
Поэтому эталон **переснимается только осознанно**, и в коммите должно быть
написано, что и почему поменялось. Молча обновлённый эталон обесценивает
весь набор.
```
python3 record_golden.py # переснять все трассы
python3 record_golden.py 7 9 # только сценарии 7 и 9
```
Трасса — по одной записи на кадр: `frame, x, y, dir, col, row, action` и
смещение `curr_seq` от `SEQTBL_BASE`. Кадр гоняется ровно в том порядке,
что и в главном цикле `roomtest.c`:
```
pop_ctrl_tick(); // ввод -> control(): смена последовательности
kid_tick(); // play_seq: следующий кадр
pop_phys_tick(); // падение/приземление/стена
pop_loose_tick(); // досчёт тряски и снятие провалившейся плиты
```
Покадровость здесь принципиальна: порт seg005/seg006 — конечный автомат, и
ошибка почти всегда проявляется не в одной функции, а в расхождении
траектории через несколько кадров. Плюс главный риск переноса на `Char`
не арифметика, а то, **кто владеет окном `Char` внутри кадра**; поймать это
можно только прогоном полного кадра.
Проверка, что харнесс воспроизводит устройство: сценарий `stand` даёт
`frame=15, x=114, y=118, curr_seq=0x19A3` — те же значения, что читаются из
`_Kid` в живом MAME.
### Как это заработало под ucsim
Три шва, каждый закрыт без правок продуктового кода:
1. **Таблицы анимации.** `pop_kid.c` ходит по ним абсолютными адресами от
`KD_DATA_OFF` (0x100) — там, куда на устройстве их приводит маппинг W0.
В тесте `kid_data.bin` просто кладётся по 0x100 (`gen_kid_blob.py`), а
`gfx_w0_map` заглушен. Следствие: код набора линкуется с
`--code-loc 0x1000` (`CODE_LOC` в Makefile), чтобы не налезть на блоб.
2. **Окружение.** `stubs.c` — libc/libbgi (графика, память, файлы), соседние
модули (`pop_bg`/`pop_trob`/`pop_level`/`pop_redraw`) и состояние стража.
Заглушки не пустые там, где это меняет смысл: файловое чтение реально
отдаёт `kid_data.bin` (иначе `pop_kid_data_load` тихо сдаётся, `kdat_ok`
остаётся нулём и `play_seq` не делает ничего — трасса выходит из одного
застывшего кадра), клавиатура отвечает по набору «нажатых» скан-кодов
(так под тест попадают `read_input` и `read_user_control`, а не только
диспетчер), а обращения к соседям **журналируются** (`tk_log`) — можно
проверять «плита отвалилась и попросила перерисовку», а не только
координаты.
3. **`__banked`.** Модули физики помечены как банковые, SDCC генерирует на
них трамплин `___sdcc_bcall_ehl`, которому нужны `set_bank`/`get_bank`.
В тесте память плоская — `bank_stub.s` отдаёт нулевой банк и пустое
переключение, соблюдая контракт по регистрам.
Что харнесс НЕ покрывает и остаётся за MAME: отрисовка и окклюзия, реальные
переключения банков, тайминги кадра, настоящая клавиатура.
## Что нужно, чтобы двинуться дальше
- **Доступ к странице уровня.** `pop_level.c` ходит по абсолютным адресам
(`(uint8_t *)(LVL_DATA_OFF + …)` после `gfx_w0_map`). Для `pop_map` это не
потребовалось — карту комнаты в него ИНЪЕКТИРУЮТ через `pop_map_set`, —
но набор на сам `pop_level`/`pop_trob` в это упрётся. Решение то же, что
для `kid_data`: положить страницу по нужному адресу и поднять `CODE_LOC`.
- **Сценарии из `bug_closed.md`.** У каждой закрытой записи есть симптом и
ожидаемое поведение — готовый список регрессионных кейсов, который стоит
переносить в `t_phys` по мере надобности.
- **BUG-LOOSE-2** (гонка «уйти из комнаты раньше, чем долетит плита») —
через мост MAME воспроизвести не удалось, а на уровне логики это
несколько строк: заспавнить кусок, сменить комнату, тикать до
приземления, проверить щебень.
@@ -0,0 +1,25 @@
;--------------------------------------------------------------------------
; bank_stub.s — set_bank/get_bank для прогона под ucsim.
;
; Модули движка помечены __banked (в продукте они лежат в страницах W3), и
; SDCC генерирует на такие вызовы трамплин ___sdcc_bcall_ehl из своей
; библиотеки. Трамплин обязан найти две пользовательские подпрограммы:
;
; get_bank — вернуть в A номер текущего банка, прочие регистры не трогать
; set_bank — переключить банк на A, прочие регистры не трогать
;
; В тесте банков нет: вся память плоская, все модули слинкованы вместе.
; Поэтому «банк» всегда нулевой, а переключение — пустое. Важно только
; соблюсти контракт по регистрам, иначе трамплин испортит аргументы вызова.
;--------------------------------------------------------------------------
.area _CODE
.globl set_bank
.globl get_bank
get_bank::
ld a, #0x00
ret
set_bank::
ret
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
gen_kid_blob.py kid_data.bin как C-массив для тестов под ucsim.
Зачем. Таблицы кадров и seqtbl живут в EMM-странице, замапленной в W0, и
pop_kid ходит по ним АБСОЛЮТНЫМИ адресами от KD_DATA_OFF (0x100). В ucsim
никакого W0 нет, зато есть ровная 64-КБ память, поэтому тест просто кладёт
файл по 0x100 и глушит gfx_w0_map. Единственное следствие для сборки
код набора линкуется с --code-loc выше конца блоба (см. Makefile).
Файл берётся ровно тот, что уходит на устройство, pop_extract_kid_data.py
делает его из оригинального PRINCE.EXE, так что тест проверяет порт против
НАСТОЯЩИХ данных игры, а не против переписанной от руки таблицы.
"""
import sys
from pathlib import Path
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
data = src.read_bytes()
out = [
"/* Сгенерировано gen_kid_blob.py — не править руками. */",
'#include "kidblob.h"',
"",
f"const unsigned char kid_blob[{len(data)}] = {{",
]
for i in range(0, len(data), 16):
out.append(" " + ",".join(str(b) for b in data[i:i + 16]) + ",")
out += [
"};",
"",
f"const unsigned int kid_blob_size = {len(data)};",
"",
"/* Разложить блоб по KD_DATA_OFF. Без memcpy: тестовый бинарь не",
" * линкуется с libc, а тянуть ради этого z80-рантайм незачем. */",
"void kid_blob_install(void)",
"{",
" unsigned char *d = (unsigned char *)KID_BLOB_ADDR;",
" const unsigned char *s = kid_blob;",
f" unsigned int n = {len(data)};",
" while (n--) *d++ = *s++;",
"}",
"",
]
dst.write_text("\n".join(out))
print(f"gen_kid_blob: {src.name} -> {dst.name} ({len(data)} Б)")
@@ -0,0 +1,22 @@
/* Эталон трассы: crouch. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x6B, 0x81, 0x76, 0x00, 0x04, 0x01, 0x01, 0x3E, 0x06,
0x6C, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x41, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6E, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4A, 0x06,
0x6F, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4B, 0x06,
0x70, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4E, 0x06,
0x71, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4F, 0x06,
0x72, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x52, 0x06,
0x73, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x53, 0x06,
0x74, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x54, 0x06,
0x75, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x57, 0x06,
0x76, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x58, 0x06,
0x77, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x59, 0x06,
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,36 @@
/* Эталон трассы: fall_ledge. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x66, 0x9E, 0x79, 0x00, 0x06, 0x02, 0x03, 0x77, 0x02,
0x67, 0xA0, 0x7F, 0x00, 0x06, 0x02, 0x03, 0x7C, 0x02,
0x68, 0x9F, 0x88, 0x00, 0x06, 0x02, 0x03, 0x81, 0x02,
0x69, 0x9F, 0x94, 0x00, 0x06, 0x02, 0x03, 0x84, 0x02,
0x6A, 0x9E, 0xA6, 0x00, 0x06, 0x02, 0x04, 0xDE, 0x03,
0x6B, 0xA0, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC3, 0x06,
0x6C, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC6, 0x06,
0x6D, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x01, 0xC9, 0x06,
0x6E, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4A, 0x06,
0x6F, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4B, 0x06,
0x70, 0xA5, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4E, 0x06,
@@ -0,0 +1,22 @@
/* Эталон трассы: jump_up. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x43, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x12, 0x04,
0x44, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x13, 0x04,
0x45, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x14, 0x04,
0x46, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x15, 0x04,
0x47, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x16, 0x04,
0x48, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x17, 0x04,
0x49, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x18, 0x04,
0x4A, 0x80, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x19, 0x04,
0x4B, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1A, 0x04,
0x4C, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1B, 0x04,
0x4D, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1C, 0x04,
0x4E, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1D, 0x04,
0x4F, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1E, 0x04,
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x21, 0x04,
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x24, 0x04,
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x25, 0x04,
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x28, 0x04,
0x51, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB8, 0x03,
0x52, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB9, 0x03,
0x53, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x05, 0xBC, 0x03,
@@ -0,0 +1,36 @@
/* Эталон трассы: loose. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
0x09, 0x9E, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xA0, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0xA4, 0x76, 0x00, 0x06, 0x01, 0x01, 0x22, 0x00,
0x0C, 0xA9, 0x76, 0x00, 0x07, 0x01, 0x01, 0x25, 0x00,
0x0D, 0xAB, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0xAE, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2D, 0x00,
0x07, 0xB2, 0x76, 0x00, 0x07, 0x01, 0x01, 0x14, 0x00,
0x08, 0xB7, 0x76, 0x00, 0x08, 0x01, 0x01, 0x17, 0x00,
0x09, 0xB8, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xBA, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1F, 0x00,
@@ -0,0 +1,36 @@
/* Эталон трассы: rjump_gap3. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x08, 0x00,
0x02, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x09, 0x00,
0x03, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0A, 0x00,
0x04, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0B, 0x00,
0x05, 0xB0, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0E, 0x00,
0x06, 0xAD, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x11, 0x00,
0x07, 0xAA, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x14, 0x00,
0x08, 0xA5, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x17, 0x00,
0x09, 0xA4, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xA2, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x9E, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x99, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x97, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x2A, 0x00,
0x22, 0x95, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x64, 0x01,
0x23, 0x90, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x67, 0x01,
0x24, 0x8A, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x6A, 0x01,
0x25, 0x87, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x6D, 0x01,
0x26, 0x82, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x72, 0x01,
0x27, 0x7B, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x75, 0x01,
0x28, 0x6F, 0x73, 0xFF, 0x04, 0x01, 0x01, 0x7A, 0x01,
0x29, 0x67, 0x6A, 0xFF, 0x03, 0x01, 0x01, 0x7F, 0x01,
0x2A, 0x5F, 0x68, 0xFF, 0x02, 0x01, 0x01, 0x84, 0x01,
0x2B, 0x5B, 0x73, 0xFF, 0x01, 0x01, 0x01, 0x89, 0x01,
0x2C, 0x57, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x8E, 0x01,
0x07, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x14, 0x00,
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
0x33, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x94, 0x04,
0x34, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x95, 0x04,
0x0F, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x00, 0x35, 0x00,
0x10, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x28, 0x01,
0x11, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x29, 0x01,
0x12, 0x50, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2C, 0x01,
0x13, 0x4E, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2F, 0x01,
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
@@ -0,0 +1,26 @@
/* Эталон трассы: run_right. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
@@ -0,0 +1,14 @@
/* Эталон трассы: stand. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,22 @@
/* Эталон трассы: step_shift. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x79, 0x72, 0x76, 0x00, 0x03, 0x01, 0x01, 0x6F, 0x05,
0x7A, 0x73, 0x76, 0x00, 0x03, 0x01, 0x01, 0x72, 0x05,
0x7B, 0x74, 0x76, 0x00, 0x03, 0x01, 0x01, 0x75, 0x05,
0x7C, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x78, 0x05,
0x7D, 0x7B, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7B, 0x05,
0x7E, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7E, 0x05,
0x7F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x81, 0x05,
0x80, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x82, 0x05,
0x81, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x83, 0x05,
0x82, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x84, 0x05,
0x83, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x85, 0x05,
0x84, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x86, 0x05,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,18 @@
/* Эталон трассы: turn. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x2D, 0x7A, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD1, 0x01,
0x2E, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD4, 0x01,
0x2F, 0x77, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD7, 0x01,
0x30, 0x78, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xDA, 0x01,
0x01, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x08, 0x00,
0x02, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x09, 0x00,
0x03, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x71, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x6E, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x11, 0x00,
0x07, 0x6B, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x66, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x17, 0x00,
0x09, 0x65, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x63, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x5F, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x5A, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x25, 0x00,
@@ -0,0 +1,18 @@
/*
* kidblob.h таблицы анимации Кида в памяти теста.
*
* KID_BLOB_ADDR обязан совпадать с KD_DATA_OFF из pop_kid.c: продукт
* адресует таблицы именно так, и в этом весь смысл под тестом работает
* тот же код доступа, что на устройстве, просто без маппинга W0.
*/
#ifndef POP_TESTS_KIDBLOB_H
#define POP_TESTS_KIDBLOB_H
#define KID_BLOB_ADDR 0x0100
extern const unsigned char kid_blob[];
extern const unsigned int kid_blob_size;
void kid_blob_install(void);
#endif
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
record_golden.py снять эталонные трассы для t_phys.
Зачем скрипт, а не один прогон. Отчёт tcheck ограничен 768 байтами лога, а
трасс на все сценарии сильно больше. Поэтому набор умеет снимать РОВНО
ОДИН сценарий за прогон (-DSC_RECORD=<n>), а перебор делает этот скрипт:
собирает набор N раз, каждый раз забирает из отчёта имя сценария и его
трассу и пишет golden/<имя>.h.
Эталон фиксирует ТЕКУЩЕЕ поведение. Это осознанно: набор страхует
поведение-сохраняющие правки (перенос физики Kid -> Char), а не доказывает
правильность. Правильность сверка с SDLPoP. Поэтому переснимать эталоны
можно только вместе с внятным объяснением, что и почему поменялось.
Использование:
python3 record_golden.py # снять все сценарии
python3 record_golden.py 7 9 # только эти номера
"""
import re
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
GOLDEN = HERE / "golden"
# Число сценариев = число TC_RUN в t_phys.c: список ведётся там, здесь его
# дублировать незачем.
NSCEN = len(re.findall(r"^\s*TC_RUN\(", (HERE / "t_phys.c").read_text(), re.M))
def record(n: int) -> str:
# make не видит смены -DSC_RECORD (в зависимостях только файлы), поэтому
# объектник набора сносим руками — иначе следующий прогон молча снимет
# трассу предыдущего сценария под новым именем.
for f in ("phys.rel", "phys.ihx"):
(HERE / "build" / f).unlink(missing_ok=True)
subprocess.run(["make", "-s", "build/phys.ihx", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
cwd=HERE, check=True)
p = subprocess.run(["make", "-s", "run-phys", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
cwd=HERE, capture_output=True, text=True)
out = p.stdout
m = re.search(r"^\s*### (\S+)\s*$", out, re.M)
if not m:
raise SystemExit(f"сценарий {n}: в отчёте нет строки '### имя'\n{out}")
name = m.group(1)
rows = re.findall(r"^\s*([0-9A-F]{18})\s*$", out, re.M)
if not rows:
raise SystemExit(f"сценарий {n} ({name}): трасса пустая\n{out}")
lines = [f"/* Эталон трассы: {name}. Снят record_golden.py — не править руками.",
" * Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */"]
for r in rows:
lines.append(" " + ", ".join(f"0x{r[i:i+2]}" for i in range(0, 18, 2)) + ",")
GOLDEN.mkdir(exist_ok=True)
(GOLDEN / f"{name}.h").write_text("\n".join(lines) + "\n")
return f"{name}: {len(rows)} кадров"
def main() -> int:
nums = [int(a) for a in sys.argv[1:]] or list(range(1, NSCEN + 1))
for n in nums:
print(f"[{n}/{NSCEN}] " + record(n))
# Итоговая пересборка в обычном режиме, чтобы каталог не остался с
# объектниками, собранными под SC_RECORD.
subprocess.run(["make", "-s", "clean"], cwd=HERE, check=True)
print("эталоны сняты; проверить: make -C", HERE.name, "t_phys")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,163 @@
/*
* scene.c реализация покадрового раннера (см. scene.h).
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h"
#include "pop_geom.h"
#include "pop_guard.h"
#include "kid_data.h"
#include "scene.h"
#include "stubs.h"
#include "kidblob.h"
sc_rec sc_trace[SC_TRACE_MAX];
uint8_t sc_len;
static uint8_t sc_fg[30];
/* Кромки шва: соседей у сцены нет, поэтому по краям стена (тайл 20). */
static const uint8_t sc_edge[6] = { 20, 20, 20, 20, 20, 20 };
void sc_init(void)
{
kid_blob_install(); /* таблицы туда, куда смотрит pop_kid */
pop_kid_data_load("KID\\KID_DATA.BIN");
pop_kid_hp_newgame();
}
void sc_room(const uint8_t *fg, uint8_t room)
{
uint8_t i;
for (i = 0; i < 30; i++) { sc_fg[i] = fg[i]; tk_modif[i] = 0; }
pop_map_set(sc_fg);
pop_map_set_room(room);
/* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон
* и сверху/снизу тогда решения физики зависят только от sc_fg, и
* трасса не поедет, если когда-нибудь поменяется загрузка кромок. */
pop_map_set_edges(0, 0, 0, 0, sc_edge, sc_edge);
tk_start_room = room;
tk_log_clear();
}
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir)
{
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
pop_kid_hp_reset();
}
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir)
{
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */
pop_determine_col(); /* колонку пересчитать под новый X */
pop_kid_hp_reset();
}
void sc_trace_clear(void) { sc_len = 0; }
static void sc_record(void)
{
sc_rec *r;
if (sc_len >= SC_TRACE_MAX) return;
r = &sc_trace[sc_len++];
r->frame = Kid.frame;
r->x = Kid.x;
r->y = Kid.y;
r->dir = Kid.direction;
r->col = Kid.curr_col;
r->row = Kid.curr_row;
r->action = Kid.action;
r->seq = (uint16_t)(Kid.curr_seq - SEQTBL_BASE);
}
uint8_t sc_run(uint8_t keys, uint8_t n)
{
uint8_t i, done = 0;
for (i = 0; i < n; i++) {
tk_keys_clear();
if (keys & SC_L) tk_key_down(KBD_LEFT);
if (keys & SC_R) tk_key_down(KBD_RIGHT);
if (keys & SC_U) tk_key_down(KBD_UP);
if (keys & SC_D) tk_key_down(KBD_DOWN);
if (keys & SC_SHIFT) tk_key_down(KBD_LSHIFT);
/* Порядок ровно как в главном цикле roomtest.c (kid_heal и
* отрисовка выпадают они ничего не решают). pop_loose_tick
* оставлен: он не рисование, а МЕХАНИКА досчитывает тряску и
* убирает провалившуюся плиту из карты, то есть меняет то, по чему
* физика в следующем кадре принимает решения. */
pop_ctrl_tick();
kid_tick();
pop_phys_tick();
pop_loose_tick();
sc_record();
done++;
if (sc_len >= SC_TRACE_MAX) break;
}
return done;
}
/* ---- сверка с эталоном ---------------------------------------------- */
void sc_expect(const uint8_t *golden, uint8_t nrec)
{
uint8_t i;
const uint8_t *g = golden;
tc_check((int16_t)sc_len, (int16_t)nrec, "sc_len", __LINE__);
for (i = 0; i < nrec && i < sc_len; i++, g += SC_REC_BYTES) {
const sc_rec *r = &sc_trace[i];
/* Номер кадра идёт первым полем каждой проверки: в отчёте видно,
* на каком именно кадре трасса разошлась. */
tc_check((int16_t)r->frame, (int16_t)g[0], "frame", i);
tc_check((int16_t)r->x, (int16_t)g[1], "x", i);
tc_check((int16_t)r->y, (int16_t)g[2], "y", i);
tc_check((int16_t)r->dir, (int16_t)(int8_t)g[3], "dir", i);
tc_check((int16_t)r->col, (int16_t)(int8_t)g[4], "col", i);
tc_check((int16_t)r->row, (int16_t)(int8_t)g[5], "row", i);
tc_check((int16_t)r->action, (int16_t)g[6], "action", i);
tc_check((int16_t)r->seq,
(int16_t)((uint16_t)g[7] | ((uint16_t)g[8] << 8)), "seq", i);
}
}
/* ---- снятие эталона -------------------------------------------------- */
static void put(char c)
{
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
}
static void put_byte(uint8_t v)
{
static const char hex[] = "0123456789ABCDEF";
put(hex[v >> 4]); put(hex[v & 15]);
}
void sc_dump(const char *name)
{
uint8_t i;
tc_result.magic = TC_MAGIC;
tc_result.total = 1;
tc_result.failed = 1; /* иначе раннер лог не напечатает */
put('#'); put('#'); put('#'); put(' ');
while (*name) put(*name++);
put('\n');
for (i = 0; i < sc_len; i++) {
const sc_rec *r = &sc_trace[i];
put_byte(r->frame);
put_byte(r->x);
put_byte(r->y);
put_byte((uint8_t)r->dir);
put_byte((uint8_t)r->col);
put_byte((uint8_t)r->row);
put_byte(r->action);
put_byte((uint8_t)r->seq);
put_byte((uint8_t)(r->seq >> 8));
put('\n');
}
}
@@ -0,0 +1,88 @@
/*
* scene.h покадровый сценарный раннер для тестов физики Кида.
*
* Почему покадровый, а не «вызвать функцию и сверить возврат». Порт
* seg005/seg006 это конечный автомат: диспетчер выбирает
* последовательность, интерпретатор проигрывает её кадр за кадром, физика
* между кадрами двигает персонажа и решает, не пора ли падать. Ошибка
* почти всегда проявляется не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ
* через несколько кадров. Плюс главный риск ближайшего рефакторинга
* (Kid -> Char) не арифметика, а то, КТО владеет окном Char внутри кадра;
* поймать это можно только прогоном полного кадра, как в главном цикле:
*
* pop_ctrl_tick(); // ввод -> control(): смена последовательности
* kid_tick(); // play_seq: следующий кадр
* pop_phys_tick(); // падение/приземление/стена
*
* Отсюда формат: сцена (комната + стартовая поза) + скрипт ввода, на
* выходе трасса состояния Кида по кадрам. Сравнение трассы с эталоном
* и есть тест.
*/
#ifndef POP_TESTS_SCENE_H
#define POP_TESTS_SCENE_H
#include <stdint.h>
/* Одна запись трассы: то, чем оперируют решения движка. curr_seq —
* СМЕЩЕНИЕ от SEQTBL_BASE (адрес зависит от базы, смещение нет). */
typedef struct {
uint8_t frame;
uint8_t x, y;
int8_t dir;
int8_t col, row;
uint8_t action;
uint16_t seq;
} sc_rec;
#define SC_TRACE_MAX 48
extern sc_rec sc_trace[SC_TRACE_MAX];
extern uint8_t sc_len;
/* Кнопки скрипта ввода — битовая маска на кадр. */
#define SC_L 0x01
#define SC_R 0x02
#define SC_U 0x04
#define SC_D 0x08
#define SC_SHIFT 0x10
/* Инициализация окружения набора: таблицы анимации, обнуление состояния.
* Звать один раз в начале прогона. */
void sc_init(void);
/* Задать комнату: fg[30] — коды тайлов по рядам (row*10+col), room —
* номер (нужен trob'ам). Модификаторы обнуляются; ставить их тест может
* сам через tk_modif. Соседи комнаты стены (изолированная сцена). */
void sc_room(const uint8_t *fg, uint8_t room);
/* Поставить Кида: колонка/ряд задают позицию по центру тайла, как это
* делает do_startpos (seg003) так сцена описывается в терминах карты, а
* не в пикселях. */
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir);
/* То же, но с точным X: тайл задаёт ряд и пол под ногами, X — фазу внутри
* тайла (нужно, когда исход зависит от того, где именно Кид стоит). */
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir);
/* Прогнать n кадров с ЗАЖАТОЙ маской keys, записывая трассу.
* Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */
uint8_t sc_run(uint8_t keys, uint8_t n);
/* Сбросить трассу (перед новым сценарием). */
void sc_trace_clear(void);
/* Сверить трассу с эталоном: golden — плоский массив по 8 байт на кадр
* (frame,x,y,dir,col,row,action,seq_lo,seq_hi 9), nrec записей.
* Расхождения уходят в отчёт tcheck с номером кадра и поля. */
void sc_expect(const uint8_t *golden, uint8_t nrec);
#define SC_REC_BYTES 9
/* Снятие эталона. Лог tcheck всего 768 байт — на все сценарии сразу трасс
* не хватит, поэтому за прогон снимается РОВНО ОДИН сценарий (его номер
* задаёт -DSC_RECORD=<n>), а перебором занимается record_golden.py.
* Формат: строка «### имя», дальше по строке на кадр, по два хекс-символа
* на байт. Раннер печатает лог только при провале sc_dump ставит
* failed=1 намеренно. */
void sc_dump(const char *name);
#endif
@@ -0,0 +1,200 @@
/*
* stubs.c окружение движка для прогона под ucsim_z80.
*
* Под тестом крутится НАСТОЯЩАЯ физика (pop_map) с настоящим
* интерпретатором последовательностей (pop_kid): это единственный способ
* проверить порт seg005/seg006, потому что вся его суть в точных
* величинах кадров и seqtbl. А вот всё, что вокруг, в ucsim не существует:
* видеопамять, EMM-страницы, файлы, клавиатура. Здесь они заглушены.
*
* Три категории:
* 1. libc/libbgi (графика, память, файлы, клавиатура) пустышки. Код
* отрисовки из pop_kid.c линкуется, но не вызывается.
* 2. Соседние модули движка (pop_bg/pop_trob/pop_level/pop_redraw)
* минимальные модели: room_modif живёт здесь массивом, пометки
* перерисовки и падающие куски ЖУРНАЛИРУЮТСЯ (тест может проверить,
* что физика их дёрнула), уровень отдаёт стартовую комнату.
* 3. Состояние стража просто данные: страж в тестах физики не тикает,
* но pop_map/pop_ctrl на его поля смотрят.
*
* Клавиатура сделана НЕ пустышкой: `tk_keys` набор «нажатых» скан-кодов,
* и `kbd_raw_down` отвечает по нему. Так под тест попадают и read_input, и
* read_user_control, то есть весь путь ввода, а не только диспетчер.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include "pop_kid.h"
#include "pop_guard.h"
#include "pop_bg.h"
#include "pop_map.h"
#include "pop_trob.h"
#include "pop_level.h"
#include "pop_redraw.h"
#include "_pop_draw.h"
#include "stubs.h"
#include "kidblob.h"
/* ---- 1. libc / libbgi ---------------------------------------------- */
uint8_t _gfx_visible_page, _gfx_draw_page, _gfx_bank;
/* ISR-стаб W0-страницы: продукт берёт его АДРЕС и вписывает в страницу.
* Здесь достаточно, чтобы символ существовал. */
void _gfx_w0_isr(void) { }
void gfx_w0_map(uint8_t page) { (void)page; }
void gfx_w0_unmap(void) { }
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
{ (void)x; (void)y; (void)img; (void)flip; }
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
int skip, int rows, uint8_t maxw)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; (void)maxw; }
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
int atlas_load(atlas_t *a, const char *path) { (void)a; (void)path; return -1; }
void atlas_free(atlas_t *a) { (void)a; }
const void *atlas_image(const atlas_t *a, uint8_t idx)
{ (void)a; (void)idx; return 0; }
/* EMM: страниц нет, но блок обязан выделиться — иначе pop_kid_data_load
* сдастся на первом же шаге и молча оставит kdat_ok = 0, а play_seq тогда
* просто ничего не делает (и трасса выходит из одного застывшего кадра). */
uint8_t mem_alloc_pages_bios(uint8_t n) { (void)n; return 1; }
void mem_free_block_bios(uint8_t blk) { (void)blk; }
uint8_t mem_get_page_bios(uint8_t blk, uint8_t idx) { (void)blk; (void)idx; return 1; }
/* Файловая система: единственный «файл» — таблицы анимации. Сделано не
* пустышкой, а рабочим чтением, чтобы pop_kid_data_load отработал целиком
* (включая правку ISR-стаба страницы) и сам взвёл свой kdat_ok тест не
* лезет во внутренние статики продукта. Данные, по которым потом ходят
* аксессоры, кладёт kid_blob_install по KID_BLOB_ADDR: на устройстве туда
* же их приводит маппинг W0. */
int open(const char *p, int f) { (void)p; (void)f; return 3; }
int read(int fd, void *b, int n)
{
unsigned char *d = (unsigned char *)b;
const unsigned char *s = kid_blob;
unsigned int i, cnt = kid_blob_size;
if (fd != 3) return -1;
if ((unsigned int)n < cnt) cnt = (unsigned int)n;
for (i = 0; i < cnt; i++) d[i] = s[i];
return (int)cnt;
}
int close(int fd) { (void)fd; return 0; }
/* ---- клавиатура: управляемая тестом --------------------------------- */
uint16_t tk_keys[TK_KEYS_MAX];
int kbd_raw_open(void) { return 0; }
void kbd_raw_close(void) { }
void kbd_raw_sync(void) { }
uint8_t kbd_raw_down(uint16_t code)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++)
if (tk_keys[i] == code) return 1;
return 0;
}
void tk_keys_clear(void)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++) tk_keys[i] = 0;
}
void tk_key_down(uint16_t code)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++)
if (tk_keys[i] == 0) { tk_keys[i] = code; return; }
}
/* ---- 2. соседние модули движка -------------------------------------- */
/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота),
* зелья (тип). Физика читает и пишет его напрямую через pop_trob_modif. */
uint8_t tk_modif[30];
uint8_t tk_start_room;
uint8_t *pop_trob_modif(uint8_t room) __banked { (void)room; return tk_modif; }
uint8_t pop_level_start_room(void) { return tk_start_room; }
/* Журнал: что физика попросила у соседей. Тест сверяет ФАКТ и параметры
* (какой тайл помечен, где заспавнен кусок) заглушка-пустышка этого не
* дала бы, а именно такие баги (BUG-GATE-ANIM-1) мы и ловим. */
tk_log_t tk_log;
static void log_put(uint8_t kind, uint8_t a, uint8_t b, uint8_t c)
{
if (tk_log.n >= TK_LOG_MAX) { tk_log.overflow = 1; return; }
tk_log.e[tk_log.n].kind = kind;
tk_log.e[tk_log.n].a = a;
tk_log.e[tk_log.n].b = b;
tk_log.e[tk_log.n].c = c;
tk_log.n++;
}
void tk_log_clear(void) { tk_log.n = 0; tk_log.overflow = 0; }
uint8_t tk_log_count(uint8_t kind)
{
uint8_t i, c = 0;
for (i = 0; i < tk_log.n; i++) if (tk_log.e[i].kind == kind) c++;
return c;
}
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
{ log_put(TK_TROB, room, tilepos, (uint8_t)type); }
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier) __banked
{ (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); }
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
{ log_put(TK_REDRAW, tilepos, kind, pages); }
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
{ log_put(TK_REDRAW_ABOVE, col, kind, pages); }
void pop_loose_mob_spawn(int row, int col) __banked
{ log_put(TK_MOB_SPAWN, (uint8_t)row, (uint8_t)col, 0); }
void pop_loose_mob_tick(void) __banked { }
void pop_loose_mob_room_changed(void) __banked { }
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{ (void)col; (void)y; return 0; }
/* Отрисовочные хвосты, которые физика дёргает напрямую. */
void pop_fore_set_clip(int x, int y, int w, int h) __banked
{ (void)x; (void)y; (void)w; (void)h; }
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
{ (void)x; (void)w; (void)sy_top; (void)h; }
void pop_heal_fast(int x, int y, int w, int h)
{ (void)x; (void)y; (void)w; (void)h; }
/* ---- 3. состояние стража -------------------------------------------- */
pop_char_t Guard;
kframe pop_gframe;
int8_t can_guard_see_kid;
int8_t guard_refrac, kid_sword_strike;
int8_t offguard, holding_sword;
/* «Кид нашумел» — физика взводит его на ударе о стену, приземлении и
* обрушенной плите (BUG-GUARD-DEAF-1); читает только автопилот стража,
* которого в наборе нет. */
int8_t is_guard_notice;
@@ -0,0 +1,43 @@
/*
* stubs.h интерфейс тестового окружения (см. stubs.c).
*
* Что тест может задавать: «нажатые» клавиши (tk_key_down) и модификаторы
* тайлов комнаты (tk_modif). Что может проверять: журнал обращений
* физики к соседним модулям (tk_log).
*/
#ifndef POP_TESTS_STUBS_H
#define POP_TESTS_STUBS_H
#include <stdint.h>
/* ---- клавиатура ------------------------------------------------------ */
#define TK_KEYS_MAX 6
extern uint16_t tk_keys[TK_KEYS_MAX];
void tk_keys_clear(void);
void tk_key_down(uint16_t code);
/* ---- комната --------------------------------------------------------- */
extern uint8_t tk_modif[30]; /* room_modif текущей комнаты */
extern uint8_t tk_start_room; /* что вернёт pop_level_start_room */
/* ---- журнал обращений ------------------------------------------------ */
#define TK_LOG_MAX 24
enum {
TK_TROB = 1, /* a=room b=tilepos c=type */
TK_BUTTON, /* a=room b=tilepos c=type */
TK_REDRAW, /* a=tilepos b=kind c=pages */
TK_REDRAW_ABOVE, /* a=col b=kind c=pages */
TK_MOB_SPAWN /* a=row b=col */
};
typedef struct { uint8_t kind, a, b, c; } tk_log_e;
typedef struct { uint8_t n, overflow; tk_log_e e[TK_LOG_MAX]; } tk_log_t;
extern tk_log_t tk_log;
void tk_log_clear(void);
/* Сколько записей данного вида в журнале (0 = не было). */
uint8_t tk_log_count(uint8_t kind);
#endif

Some files were not shown because too many files have changed in this diff Show More