24 Commits

Author SHA1 Message Date
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
52 changed files with 3401 additions and 389 deletions
+43
View File
@@ -0,0 +1,43 @@
# Идеи и вопросы «на подумать» (PoP)
Не план работ, а список того, что осознанно отложено: каждая запись —
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
сделать это прямо сейчас.
## Заменить генератор псевдослучайных чисел
Сейчас стоит 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`).
+10 -1
View File
@@ -18,7 +18,7 @@
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) | | 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | 41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой | | 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | 41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже | | 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
| 4. Разгрузка/перебалансировка W3 | **сделан** | в резиденте остался ТОЛЬКО `pop_bg`; W3 14 376 → 11 632 (свободно 4 752 Б) | | 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 КБ | | 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже | | 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» | | 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
@@ -27,6 +27,15 @@
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение. на ~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) ### Что сделано вместо §5.3 (вынос loose в W3)
Вместо переноса кода между окнами выбран (по обсуждению с пользователем) Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
+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, и
цена вопроса вырастет во столько же раз.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -37,5 +37,5 @@ static const char *const kid_atl[KID_PAGES] = {
}; };
#define KID_PAL_FILE "kid.pal" #define KID_PAL_FILE "kid.pal"
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */ #define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
#define KID_SWORD_ID0 20 /* индекс в атласе = sword_tbl.id - ID0 */ #define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
#endif #endif
Binary file not shown.
Binary file not shown.
+39 -10
View File
@@ -5,14 +5,23 @@ PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := roomtest EXAMPLE := roomtest
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе # small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). # CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
MEMORY ?= small MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
# --w3 берёт ОДИН файл на флаг. В W3-резиденте держим ТОЛЬКО pop_bg — # Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_gdraw
# отрисовку тайлов: она большая и её зовёт лишь главный цикл. Всё # в одной странице — вызовы между ними прямые, если функция не помечена
# остальное живёт в W1/W2, чтобы оставаться достижимым из будущих # __banked). Так снято главное ограничение резидента: из банка его было
# __banked-модулей (стражи): из банка резидентная страница W3 не видна — # не достать ни прямо, ни транзитивно, а банк→банк работает (трамплин
# ни напрямую, ни транзитивно. См. docs/layout_plan_v2.md §2 (R2), §4. # сохраняет страницу окна на СТЕКЕ, проверено вложенной цепочкой
EXTRA_FLAGS ?= --gfx 256 --w3 pop_bg.c # банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c pop_map.c pop_trob.c pop_redraw.c pop_geom.c # логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
# sdcc_banked_call_rules.
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c $(PROF_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
# MAME = время фазы в растровых строках.
PROF ?= 1
PROF_FLAGS := -DPROF_BORDER=$(PROF)
EXTRA_SRCS := pop_state.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid KID_DIR := $(CURDIR)/../poc/res/kid
@@ -30,16 +39,36 @@ KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl
# Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его # Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его
# делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже. # делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже.
KID_BIN := $(KID_DIR)/kid_data.bin 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) $(KID_BIN) $(LVL_DATA) EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(LVL_DATA)
include $(PROJ_ROOT)/app.mk 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)) \
LEVELS:$(LVL_DATA)
TC := $(PROJ_ROOT)/applications/PoP/toolchain TC := $(PROJ_ROOT)/applications/PoP/toolchain
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py $(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
cd $(TC) && python3 pop_pack_bg.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 $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
cd $(TC) && python3 pop_pack_kid.py cd $(TC) && python3 pop_pack_kid.py
kid_data.h $(KID_BIN): $(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 cd $(TC) && python3 pop_extract_kid_data.py
$(EXAMPLE).exe: 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) $(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c 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)
+499
View File
@@ -0,0 +1,499 @@
/*
* 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();
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
* выбирается по Char.charid внутри load_frame (seg006:0293). */
play_seq();
pop_saveshad();
}
+31
View File
@@ -18,6 +18,10 @@ typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe;
/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */ /* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */
#define KID_BIN_FRAMES_OFF 0 #define KID_BIN_FRAMES_OFF 0
#define KID_BIN_SEQTBL_OFF 1205 #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 #define KID_NSEQ 115
static const uint16_t kid_seq_off[KID_NSEQ] = { static const uint16_t kid_seq_off[KID_NSEQ] = {
@@ -38,4 +42,31 @@ static const uint16_t kid_seq_off[KID_NSEQ] = {
0x21FC,0x2240,0x2257, 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 #endif
+277 -89
View File
@@ -14,6 +14,7 @@
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include "pop_bg.h" #include "pop_bg.h"
#include "pop_state.h"
#include "pop_geom.h" #include "pop_geom.h"
#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ #include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */ #include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */
@@ -25,22 +26,25 @@ static atlas_t fore_at;
static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */ static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */
static const char *const ENV_ATL[5] = { static const char *const ENV_ATL[5] = {
"pop_env0.atl", "pop_env1.atl", "pop_env2.atl", /* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон,
"pop_env3.atl", "pop_env4.atl" * KID — персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска
* зарастает десятками .atl. */
"BG\\pop_env0.atl", "BG\\pop_env1.atl", "BG\\pop_env2.atl",
"BG\\pop_env3.atl", "BG\\pop_env4.atl"
}; };
int pop_bg_load(void) int pop_bg_load(void) __banked
{ {
int i; int i;
for (i = 0; i < 5; i++) for (i = 0; i < 5; i++)
if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1; if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1;
if (atlas_load(&wall_at, "pop_wall.atl") != 0) return -1; if (atlas_load(&wall_at, "BG\\pop_wall.atl") != 0) return -1;
if (atlas_load(&fore_at, "pop_fore.atl") != 0) return -1; if (atlas_load(&fore_at, "BG\\pop_fore.atl") != 0) return -1;
if (atlas_load(&pot_at, "pop_pot.atl") != 0) return -1; if (atlas_load(&pot_at, "BG\\pop_pot.atl") != 0) return -1;
return 0; return 0;
} }
void pop_bg_free(void) void pop_bg_free(void) __banked
{ {
int i; int i;
for (i = 0; i < 5; i++) for (i = 0; i < 5; i++)
@@ -59,33 +63,73 @@ static int g_clip_top;
/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x, /* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x,
* прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0). * прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0).
* При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */ * При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */
/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока оно включено,
* blit_b рисует только ту часть куска, что попадает в окно.
*
* Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов
* поверх нарисованного персонажа — за пределами его прямоугольника в
* видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ:
* стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход
* до девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход
* поверх Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей
* работы кадра при бюджете 430 000 тактов на кадр. С окном большинство
* кусков отсекается целиком, остальные режутся до пересечения. */
static int fclip_x0, fclip_y0, fclip_x1, fclip_y1;
static uint8_t fclip_on;
void pop_fore_set_clip(int x, int y, int w, int h) __banked
{
fclip_x0 = x; fclip_y0 = y;
fclip_x1 = x + w; fclip_y1 = y + h;
}
static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
{ {
const uint8_t *img; const uint8_t *img;
uint16_t w, h; uint16_t w, h;
int top; int top, dx, dy, dw, dh, sx = 0, sy = 0;
if (idx >= a->count) if (idx >= a->count)
return; return;
/* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
* ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
* gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее
* 64×64) и, если он не задевает окно, уходим не заплатив ни за
* atlas_image, ни за map/unmap. */
if (fclip_on &&
(x >= fclip_x1 || x + 64 <= fclip_x0 ||
ybottom + POP_YOFF + 1 <= fclip_y0 ||
ybottom + POP_YOFF - 64 >= fclip_y1))
return;
img = (const uint8_t *)atlas_image(a, idx); img = (const uint8_t *)atlas_image(a, idx);
gfx_w0_map(a->page); gfx_w0_map(a->page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */ top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
if (w && h) { dx = x; dy = top; dw = (int)w; dh = (int)h;
if (g_clip_top && top < g_clip_top) { if (g_clip_top && dy < g_clip_top) { /* полоса у потолка */
int sy = g_clip_top - top; /* пропустить верхние строки */ sy = g_clip_top - dy; dy = g_clip_top; dh -= sy;
if (sy < (int)h) }
gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy); if (fclip_on) { /* окно fore-слоя */
} else if (x >= 0 && top >= 0 && w < 256 && h < 256 && if (dx < fclip_x0) { int d = fclip_x0 - dx; sx += d; dx += d; dw -= d; }
x + (int)w <= 320 && top + (int)h <= 256) { if (dy < fclip_y0) { int d = fclip_y0 - dy; sy += d; dy += d; dh -= d; }
/* Спрайт целиком на экране — линейное ядро без клипа: ~2.9× if (dx + dw > fclip_x1) dw = fclip_x1 - dx;
* быстрее (замер: 4617 против 13288 тактов на спрайт 32×3; if (dy + dh > fclip_y1) dh = fclip_y1 - dy;
* общее ядро gfx_blit платит за клип/16-бит/split, а не за }
* пиксели). W3-скобку ставит сама libbgi: из кода, собранного if (w && h && dw > 0 && dh > 0) {
* с --w3, её вызывать нельзя (окно W3 уходит под видеобанк). */ /* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее
gfx_blit_noclip(x, top, img); * (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро
* gfx_blit платит за клип/16-бит/split, а не за пиксели).
* W3-скобку ставит сама libbgi: из кода, собранного с --w3, её
* вызывать нельзя (окно W3 уходит под видеобанк). */
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h)
gfx_blit_noclip(dx, dy, img);
else
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
(uint8_t)dw, (uint8_t)dh);
} else { } else {
gfx_blit(x, top, img); /* край экрана — с клипом */ gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */
} }
} }
gfx_w0_unmap(); gfx_w0_unmap();
@@ -126,7 +170,7 @@ static void ov_mark(int x, int y, int w, int h)
ov_pending = 3; /* обе страницы */ ov_pending = 3; /* обе страницы */
} }
void pop_fore_heal(void) void pop_fore_heal(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1; uint8_t pg = gfx_get_draw_page() & 1;
if (!(ov_pending & (1 << pg))) return; if (!(ov_pending & (1 << pg))) return;
@@ -250,12 +294,12 @@ static uint8_t g_room;
* floorright, когда тайл нижнего ряда стал видимым (empty после падения * floorright, когда тайл нижнего ряда стал видимым (empty после падения
* loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты * loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты
* снизу (кромка уровня = стена). */ * снизу (кромка уровня = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) { g_below = below_row0_fg; } void pop_room_set_below(const uint8_t *below_row0_fg) __banked { g_below = below_row0_fg; }
/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки /* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки
* у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom * у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */ * при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
{ g_above = above_row2_fg; g_abovemod = above_row2_mod; } { g_above = above_row2_fg; g_abovemod = above_row2_mod; }
static uint8_t tile_code(int row, int col) static uint8_t tile_code(int row, int col)
@@ -310,7 +354,7 @@ static uint8_t wall_modifier(int row, int col)
} }
/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */ /* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */
static unsigned long rnd_seed; static pop_rnd_t rnd_seed;
#define prandom(maxv) pop_prandom(&rnd_seed, (maxv)) #define prandom(maxv) pop_prandom(&rnd_seed, (maxv))
@@ -335,65 +379,135 @@ static void draw_right_mark(uint16_t dv, int arg1, int row, int col)
wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]); wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]);
} }
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
/* КЭШ РАСКЛАДКИ КЛАДКИ. Серия prandom для тайла зависит только от
* (комната, ряд, колонка) — пока мы в комнате, это КОНСТАНТА. А
* wall_pattern зовётся из fore-слоя КАЖДЫЙ кадр, и на Z80 это самая
* дорогая вещь в кадре: замер в MAME 2026-07-29 (комната 3) — 18 вызовов
* pop_prandom за один fore-проход, ~10 700 тактов каждый (32-битный LCG:
* __mullong ~8 000 + __moduint), то есть 193 К из 341 К всего прохода при
* бюджете кадра ~430 К. Кэшируем не сид, а ГОТОВЫЕ РЕШЕНИЯ (что рисовать
* и с каким смещением) — тогда в кадре не остаётся ни одного prandom.
*
* Порядок вызовов prandom воспроизведён один в один, включая то, что
* значение метки берётся ТОЛЬКО когда сработало её условие: иначе
* последовательность разъедется и кладка станет другой. which_part=0
* использует префикс той же серии, поэтому кэш общий.
*
* Упаковка: wp_a = mid_div | mid_off<<1 | bot_div<<4 | bot_off<<5;
* wp_b = rndblk | right<<1 | right_val<<2 | left<<4 | left_val<<5.
* Индекс — (row+1)*10+col, ряды 1..2. */
#define WP_N 40
static uint8_t wp_a[WP_N], wp_b[WP_N], wp_ok[WP_N];
static void wall_pattern_reset(void) /* смена комнаты — сид другой */
{
uint8_t i;
for (i = 0; i < WP_N; i++) wp_ok[i] = 0;
}
/* Выходы wall_rnd — в файловых статиках: возвращать девять значений
* через указатели на Z80 дороже самой функции. */
static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off;
static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv;
static void wall_rnd(int row, int col)
{
/* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
uint8_t a, b, bg;
pop_rnd_t saved;
if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
a = wp_a[idx]; b = wp_b[idx];
wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7);
wr_bot_div = (uint8_t)((a >> 4) & 1); wr_bot_off = (uint8_t)((a >> 5) & 7);
wr_rndblk = (uint8_t)(b & 1);
wr_right = (uint8_t)((b >> 1) & 1); wr_rightv = (uint8_t)((b >> 2) & 3);
wr_left = (uint8_t)((b >> 4) & 1); wr_leftv = (uint8_t)((b >> 5) & 7);
return;
}
saved = rnd_seed;
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col);
prandom(1); /* сброс */
wr_mid_div = (uint8_t)prandom(1);
wr_mid_off = (uint8_t)prandom(4);
wr_bot_div = (uint8_t)prandom(1);
wr_bot_off = (uint8_t)prandom(4);
wr_rndblk = wr_right = wr_left = 0;
wr_rightv = wr_leftv = 0;
bg = wall_modifier(row, col);
if (bg == 3) { /* WWW */
wr_rndblk = (uint8_t)(prandom(4) == 0);
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
} else if (bg == 0) { /* SWS */
if (prandom(6) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(1); }
} else if (bg == 1) { /* SWW */
wr_rndblk = (uint8_t)(prandom(4) == 0);
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(3); }
} else { /* WWS (bg==2) */
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(1) + 2); }
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
}
rnd_seed = saved;
if (idx >= 0 && idx < WP_N) {
wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) |
(wr_bot_div << 4) | (wr_bot_off << 5));
wp_b[idx] = (uint8_t)(wr_rndblk | (wr_right << 1) | (wr_rightv << 2) |
(wr_left << 4) | (wr_leftv << 5));
wp_ok[idx] = 1;
}
}
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */ /* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
static void wall_pattern(int row, int col, int which_part) static void wall_pattern(int row, int col, int which_part)
{ {
int xh = COL_XH[col]; int xh = COL_XH[col];
int dby = 63 * row + 65; int dby = 63 * row + 65;
unsigned long saved = rnd_seed; uint8_t bg = wall_modifier(row, col);
uint16_t mid_div, mid_off, bot_div, bot_off;
uint8_t bg;
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col);
prandom(1); /* сброс */
mid_div = prandom(1);
mid_off = prandom(4);
bot_div = prandom(1);
bot_off = prandom(4);
bg = wall_modifier(row, col);
wall_rnd(row, col);
if (bg == 3) { /* WWW */ if (bg == 3) { /* WWW */
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_rndblk)
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
} }
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_right)
draw_right_mark(prandom(3), mid_off, row, col); draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (prandom(4) == 0) if (wr_left)
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col); draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
} }
} else if (bg == 0) { /* SWS */ } else if (bg == 0) { /* SWS */
if (which_part) { if (which_part && wr_left)
if (prandom(6) == 0) draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
draw_left_mark(prandom(1), mid_off - mid_div, bot_off - bot_div, row, col);
}
} else if (bg == 1) { /* SWW */ } else if (bg == 1) { /* SWW */
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_rndblk)
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
if (prandom(4) == 0) if (wr_right)
draw_right_mark(prandom(3), mid_off, row, col); draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (prandom(4) == 0) if (wr_left)
draw_left_mark(prandom(3), mid_off - mid_div, bot_off - bot_div, row, col); draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
} }
} else { /* WWS (bg==2) */ } else { /* WWS (bg==2) */
if (which_part) if (which_part)
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby); wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
if (which_part) { if (which_part) {
if (prandom(4) == 0) if (wr_right)
draw_right_mark((uint16_t)(prandom(1) + 2), mid_off, row, col); draw_right_mark(wr_rightv, wr_mid_off, row, col);
if (prandom(4) == 0) if (wr_left)
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col); draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
} }
} }
rnd_seed = saved;
} }
/* topright тайла снизу-слева (seg008.c:410). */ /* topright тайла снизу-слева (seg008.c:410). */
@@ -633,7 +747,7 @@ static void draw_tile(int row, int col)
* Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
* ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif * ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif
* внутри draw_tile). */ * внутри draw_tile). */
void pop_loose_shake_draw(int row, int col) void pop_loose_shake_draw(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
/* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх — /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
@@ -655,7 +769,7 @@ void pop_loose_shake_draw(int row, int col)
* читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать * читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать
* на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile. * на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile.
* heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */ * heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */
void pop_spike_redraw(int row, int col) void pop_spike_redraw(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
/* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ /* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ
@@ -682,7 +796,7 @@ void pop_spike_redraw(int row, int col)
* room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake): * room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake):
* heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары
* оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
void pop_room_redraw_seam_left(void) void pop_room_redraw_seam_left(void) __banked
{ {
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
@@ -704,7 +818,7 @@ void pop_room_redraw_seam_left(void)
* после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */ * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
#define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */ #define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */
void pop_ceil_shake_draw(int col) void pop_ceil_shake_draw(int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */
@@ -716,7 +830,7 @@ void pop_ceil_shake_draw(int col)
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
void pop_ceil_bake_empty(int col) void pop_ceil_bake_empty(int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
@@ -739,7 +853,7 @@ void pop_ceil_bake_empty(int col)
* draw_main_y 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька * draw_main_y 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька
* potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y * potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y
* (pot_size<<2) 14 (seg008:668). */ * (pot_size<<2) 14 (seg008:668). */
void pop_torch_draw(int row, int col, uint8_t modif) void pop_torch_draw(int row, int col, uint8_t modif) __banked
{ {
/* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1 /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1
* (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env
@@ -756,7 +870,7 @@ void pop_torch_draw(int row, int col, uint8_t modif)
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
void pop_potion_draw(int row, int col, uint8_t modif) void pop_potion_draw(int row, int col, uint8_t modif) __banked
{ {
uint8_t type = potion_type(modif); uint8_t type = potion_type(modif);
int pot_size = (type >= 2 && type < 5) ? 1 : 0; int pot_size = (type >= 2 && type < 5) ? 1 : 0;
@@ -774,7 +888,7 @@ void pop_potion_draw(int row, int col, uint8_t modif)
* (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный +
* redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
* в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */
void pop_floor_bake(int row, int col) void pop_floor_bake(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
int yb = POP_YOFF + 63 * row; int yb = POP_YOFF + 63 * row;
@@ -792,13 +906,13 @@ void pop_floor_bake(int row, int col)
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
void pop_button_redraw(int row, int col) { pop_floor_bake(row, col); } void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); }
/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка, /* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка,
* см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет, * см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет,
* а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое. * а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое.
* Куски двери непрозрачные — wipe не нужен. */ * Куски двери непрозрачные — wipe не нужен. */
void pop_leveldoor_redraw(int row, int col) void pop_leveldoor_redraw(int row, int col) __banked
{ {
gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(row, col); draw_tile(row, col);
@@ -811,7 +925,7 @@ void pop_leveldoor_redraw(int row, int col)
* рисовал loose-правый-край env_b(42); теперь пусто → чистая грань). * рисовал loose-правый-край env_b(42); теперь пусто → чистая грань).
* g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые * g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые
* пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */ * пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
void pop_loose_bake_empty(int row, int col) void pop_loose_bake_empty(int row, int col) __banked
{ {
int x = COL_XH[col] * 8; int x = COL_XH[col] * 8;
int xr = COL_XH[col + 1] * 8; int xr = COL_XH[col + 1] * 8;
@@ -874,7 +988,8 @@ static mob_t mobs[MOB_MAX];
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на /* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
* который он лёг → приложение/pop_map ставит debris + do_knock. */ * который он лёг → приложение/pop_map ставит debris + do_knock. */
uint8_t pop_loose_landed; /* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а
* писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */
/* y_loose_land / y_something (seg007): [row+1], row = 1..3. Ряд 1 — /* y_loose_land / y_something (seg007): [row+1], row = 1..3. Ряд 1 —
* плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный * плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный
@@ -883,7 +998,7 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25}; static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF #define MOB_Y_NONE 0x7FFF
void pop_loose_mob_spawn(int row, int col) void pop_loose_mob_spawn(int row, int col) __banked
{ {
uint8_t i; uint8_t i;
mob_t *m = 0; mob_t *m = 0;
@@ -920,7 +1035,7 @@ static int8_t obj_col, obj_row;
static void mob_render(mob_t *m, uint8_t pg); static void mob_render(mob_t *m, uint8_t pg);
void pop_loose_mob_reset(void) void pop_loose_mob_reset(void) __banked
{ {
uint8_t i; uint8_t i;
for (i = 0; i < MOB_MAX; i++) { for (i = 0; i < MOB_MAX; i++) {
@@ -985,7 +1100,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
mob_render(m, pg); mob_render(m, pg);
} }
void pop_loose_mob_tick(void) void pop_loose_mob_tick(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1, i; uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++) for (i = 0; i < MOB_MAX; i++)
@@ -1035,7 +1150,7 @@ static void mob_render(mob_t *m, uint8_t pg)
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать /* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
* после kid_draw, до fore-слоя. */ * после kid_draw, до fore-слоя. */
void pop_loose_mob_draw_over(void) void pop_loose_mob_draw_over(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1, i; uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++) for (i = 0; i < MOB_MAX; i++)
@@ -1044,7 +1159,7 @@ void pop_loose_mob_draw_over(void)
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и /* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
* y (низ куска). 0 — куска нет. */ * y (низ куска). 0 — куска нет. */
uint8_t pop_loose_mob_pos(int *col, int *y) uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{ {
uint8_t i; uint8_t i;
for (i = 0; i < MOB_MAX; i++) for (i = 0; i < MOB_MAX; i++)
@@ -1059,7 +1174,7 @@ uint8_t pop_loose_mob_pos(int *col, int *y)
* кадрах падения — гейт по флагу). */ * кадрах падения — гейт по флагу). */
static uint8_t border_dirty; static uint8_t border_dirty;
void pop_clip_sprite(int x, int w, int sy_top, int h) void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
{ {
int sy_bot = sy_top + h; int sy_bot = sy_top + h;
(void)x; (void)w; (void)x; (void)w;
@@ -1071,7 +1186,7 @@ void pop_clip_sprite(int x, int w, int sy_top, int h)
* вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает * вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает
* чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу * чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу
* + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */ * + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */
void pop_room_clip_borders(void) void pop_room_clip_borders(void) __banked
{ {
if (!border_dirty) return; if (!border_dirty) return;
border_dirty--; border_dirty--;
@@ -1081,10 +1196,11 @@ void pop_room_clip_borders(void)
void pop_room_draw(uint8_t room_num, void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg, 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
{ {
int row, col; int row, col;
g_room = room_num; g_room = room_num;
wall_pattern_reset(); /* кэш кладки привязан к номеру комнаты */
g_fg = fg; g_bg = bg; g_fg = fg; g_bg = bg;
g_lfg = lcol_fg; g_lbg = lcol_bg; g_lfg = lcol_fg; g_lbg = lcol_bg;
@@ -1174,8 +1290,25 @@ static void ceil_over_kid_tile(int col)
fore_only_tile(-1, col); /* draw_tile_fore */ fore_only_tile(-1, col); /* draw_tile_fore */
} }
/* Пересекается ли габарит тайла с окном fore-слоя. Шаг тайла — 32 px
* (COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры
* и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра.
* Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной
* задевает не все тайлы футпринта, а платили за каждый (atlas_image +
* gfx_w0_map + разбор заголовка на КАЖДЫЙ кусок). */
static uint8_t tile_in_fclip(int row, int col)
{
int x0, y0;
if (!fclip_on) return 1;
x0 = COL_XH[col] * 8;
y0 = POP_YOFF + 63 * row;
return (uint8_t)(x0 < fclip_x1 && x0 + 40 > fclip_x0 &&
y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0);
}
static void fore_tile(int row, int col) /* передний слой одного тайла */ static void fore_tile(int row, int col) /* передний слой одного тайла */
{ {
if (!tile_in_fclip(row, col)) return;
/* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал /* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
* кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в * кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
* backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ * backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ
@@ -1208,7 +1341,9 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx)
{ {
/* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую /* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
* Kid спускается/подтягивается, закрашивается целым тайлом */ * Kid спускается/подтягивается, закрашивается целым тайлом */
uint8_t code = tile_code_drawn(row, col); uint8_t code;
if (!tile_in_fclip(row, col)) return;
code = tile_code_drawn(row, col);
int x, dmy, dby; int x, dmy, dby;
const piece *t; const piece *t;
if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */ if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */
@@ -1298,8 +1433,9 @@ static void overlay_mid_tile(int row, int col)
* сразу), поэтому порядок эмулируется явной проверкой. */ * сразу), поэтому порядок эмулируется явной проверкой. */
static void other_overlay_tile(int row, int col) static void other_overlay_tile(int row, int col)
{ {
uint8_t code = tile_code(row, col); uint8_t code, left;
uint8_t left; if (!tile_in_fclip(row, col)) return;
code = tile_code(row, col);
/* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для /* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для
* него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань * него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань
* пустого соседа). В оригинале она уходит в y-сортируемый midtable и * пустого соседа). В оригинале она уходит в y-сортируемый midtable и
@@ -1322,13 +1458,20 @@ static void other_overlay_tile(int row, int col)
} }
} }
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, /* Футпринт персонажа в тайлах (порт set_char_collision, seg006:0723).
int8_t dir, uint8_t frame, uint8_t action) * Выделено из pop_fore_over_kid, потому что тем же счётом пользуется страж
* (pop_fore_over_char): в оригинале это общий код для любого Char.
* Результат — в файловых статиках, а не через 6 указателей: на Z80 такой
* возврат стоил бы дороже самой функции. */
static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw;
static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir)
{ {
int wh = (int)((w + 1) >> 1); /* char_width_half */ int wh = (int)((w + 1) >> 1); /* char_width_half */
int xl = obj_x / 2 + 58; /* char_x_left (58-база) */ int xl = obj_x / 2 + 58; /* char_x_left (58-база) */
int xr; int xr;
int8_t cL, cR, cLraw, rT, rB, r, c, rTraw; int8_t cL, cR, cLraw, rT, rB, rTraw;
if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; xr = xl + wh;
cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */ cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */
@@ -1356,6 +1499,18 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
* (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим * (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим
* включение ряда rB-1. */ * включение ряда rB-1. */
if (rB > 0 && rT >= rB) rT = rB - 1; if (rB > 0 && rT >= rB) rT = rB - 1;
fp_cL = cL; fp_cR = cR; fp_cLraw = cLraw;
fp_rT = rT; fp_rB = rB; fp_rTraw = rTraw;
}
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) __banked
{
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir);
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт /* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
* Kid попадает в midtable — база порядка окклюзии в other_overlay_tile. * Kid попадает в midtable — база порядка окклюзии в other_overlay_tile.
* run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/ * run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/
@@ -1423,4 +1578,37 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
g_clip_top = 0; g_clip_top = 0;
} }
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
fclip_on = 0;
}
/* Передние грани тайлов поверх ПРОИЗВОЛЬНОГО персонажа (страж, дальше —
* тень/скелет). В оригинале любой Char — запись midtable, а foretable
* рисуется после всех midtable (draw_tile_fore, seg008:690), поэтому
* колонна и кромка пола перекрывают всех одинаково.
*
* Отличие от pop_fore_over_kid: только слой fore (+ полоса потолка).
* Оверлеи тела пола (draw_other_overlay) завязаны на позы Kid — вис,
* полёт, подъём, — которых у стража нет: он ходит и дерётся на полу
* (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
* порт redraw_at_char2, а не догадки сейчас. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir) __banked
{
int8_t r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir);
gfx_set_bank(GFX_BANK_SPRITE);
for (r = fp_rT; r <= fp_rB; r++)
for (c = fp_cL; c <= fp_cR; c++)
fore_tile(r, c);
if (fp_rTraw < 0) {
g_clip_top = POP_YOFF;
for (c = fp_cL; c <= fp_cR; c++) {
ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H);
ceil_over_kid_tile(c);
}
g_clip_top = 0;
}
gfx_set_bank(GFX_BANK_NORMAL);
fclip_on = 0;
} }
+58 -28
View File
@@ -25,13 +25,26 @@
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */ /* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H) #define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
* того, вылезает край или нет.
* static inline, а не чистый inline: SDCC оставляет мёртвое тело в каждом TU
* (memory sdcc_inline_codegen_findings), но здесь это ~30 байт на два TU —
* дешевле риска неразрешённой ссылки. */
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);
}
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load /* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ * трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */ * после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
int pop_bg_load(void); int pop_bg_load(void) __banked;
/* Освободить EMM-страницы атласов. */ /* Освободить EMM-страницы атласов. */
void pop_bg_free(void); void pop_bg_free(void) __banked;
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы /* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
* (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца * (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца
@@ -39,97 +52,114 @@ void pop_bg_free(void);
* тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */ * тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */
void pop_room_draw(uint8_t room_num, void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg, 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 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright, /* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright,
* когда тайл нижнего ряда стал empty (после падения loose): «стена вниз» * когда тайл нижнего ряда стал empty (после падения loose): «стена вниз»
* рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */ * рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */
void pop_room_set_below(const uint8_t *below_row0_fg); void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы /* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет * кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
* комнаты сверху (кромка уровня → пол). */ * комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod); void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него — /* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ * звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики * спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
* берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */ * берутся из 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, 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) __banked;
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ /* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за * (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице * прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */ * дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
void pop_fore_heal(void); void pop_fore_heal(void) __banked;
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в /* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick. * pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту * shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
* после падения (NORMAL, в ОЗУ-копию). */ * после падения (NORMAL, в ОЗУ-копию). */
void pop_loose_shake_draw(int row, int col); void pop_loose_shake_draw(int row, int col) __banked;
void pop_loose_bake_empty(int row, int col); void pop_loose_bake_empty(int row, int col) __banked;
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в /* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала. * полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
* Состояние — pop_ceil_modif[10] в pop_map. */ * Состояние — pop_ceil_modif[10] в pop_map. */
void pop_ceil_shake_draw(int col); void pop_ceil_shake_draw(int col) __banked;
void pop_ceil_bake_empty(int col); void pop_ceil_bake_empty(int col) __banked;
/* Кусок приземлился (loose_land): 0 = нет, иначе tilepos+1 тайла, на который /* Кусок приземлился (loose_land) — переехал в pop_state.h: переменную
* он лёг. Ставит pop_loose_mob_tick, разбирает pop_loose_tick (debris+knock). */ * читают по обе стороны банковой границы. */
extern uint8_t pop_loose_landed; #include "pop_state.h"
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому /* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */ * modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
void pop_spike_redraw(int row, int col); void pop_spike_redraw(int row, int col) __banked;
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них): /* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела * modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
* (пламя рисуется в ячейке правого соседа). */ * (пламя рисуется в ячейке правого соседа). */
void pop_torch_draw(int row, int col, uint8_t modif); void pop_torch_draw(int row, int col, uint8_t modif) __banked;
void pop_potion_draw(int row, int col, uint8_t modif); void pop_potion_draw(int row, int col, uint8_t modif) __banked;
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева. /* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла * g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */ * на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
void pop_room_redraw_seam_left(void); void pop_room_redraw_seam_left(void) __banked;
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице /* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте: * (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
* кнопка нажата/отжата, floor → debris от упавшей плиты. */ * кнопка нажата/отжата, floor → debris от упавшей плиты. */
void pop_floor_bake(int row, int col); void pop_floor_bake(int row, int col) __banked;
void pop_button_redraw(int row, int col); /* = pop_floor_bake (кнопка) */ void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кнопка) */
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с /* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
* запечкой — створка анимируется (pop_trob animate_leveldoor). */ * запечкой — створка анимируется (pop_trob animate_leveldoor). */
void pop_leveldoor_redraw(int row, int col); void pop_leveldoor_redraw(int row, int col) __banked;
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла /* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick * (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col); void pop_loose_mob_spawn(int row, int col) __banked;
void pop_loose_mob_tick(void); void pop_loose_mob_tick(void) __banked;
void pop_loose_mob_reset(void); /* остановить кусок (смена комнаты) */ void pop_loose_mob_reset(void) __banked; /* остановить кусок (смена комнаты) */
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs, /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после kid_draw, перед fore-слоем. */ * seg008:1561). Звать после kid_draw, перед fore-слоем. */
void pop_loose_mob_draw_over(void); void pop_loose_mob_draw_over(void) __banked;
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */ /* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
uint8_t pop_loose_mob_pos(int *col, int *y); uint8_t pop_loose_mob_pos(int *col, int *y) __banked;
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом /* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше * sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра. * верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */ * Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
void pop_clip_sprite(int x, int w, int sy_top, int h); void pop_clip_sprite(int x, int w, int sy_top, int h) __banked;
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах, /* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite): * где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать * даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */ * КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
void pop_room_clip_borders(void); void pop_room_clip_borders(void) __banked;
#endif #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
+23
View File
@@ -0,0 +1,23 @@
/*
* pop_cheat.h — режим читов (порт cheats_enabled, seg000:111).
*
* В оригинале включается ключом командной строки megahit/debug и меняет
* набор доступных клавиш. У нас конфигурации/UI пока нет, поэтому это
* просто глобальный флаг: main ставит его при старте (на время разработки —
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
*
* Реализовано: K — убить стража (seg000:786). Остальные читы оригинала
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
*/
#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) — выдать Киду меч */
#endif
+260 -19
View File
@@ -19,6 +19,7 @@
#include <kbd_raw.h> #include <kbd_raw.h>
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_map.h" #include "pop_map.h"
#include "pop_guard.h" /* charid, состояние меча, seq стража */
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в /* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2 * MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
@@ -51,6 +52,7 @@
#define SEQ_49_STAND_UP_FROM_CROUCH 49 #define SEQ_49_STAND_UP_FROM_CROUCH 49
#define SEQ_50_CROUCH 50 #define SEQ_50_CROUCH 50
#define SEQ_79_CROUCH_HOP 79 #define SEQ_79_CROUCH_HOP 79
#define SEQ_78_DRINK 78 /* выпить зелье */
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */ #define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */ #define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */ #define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
@@ -74,14 +76,21 @@
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный * X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между * автомат held/released/ignore (авто-повтор); ctrl1_* — их память между
* кадрами (rest/save). */ * кадрами (rest/save). */
static int8_t control_x, control_y, control_shift; int8_t control_x, control_y, control_shift;
static int8_t control_forward, control_backward, control_up, control_down, control_shift2; /* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же
* control(), что и у игрока. Поэтому шина общая. */
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2; static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ * /* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl * Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
* не дублируются в этот TU). */ * не дублируются в этот 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) static int8_t release_arrows(void)
{ {
@@ -118,6 +127,11 @@ static uint8_t get_item_action(void)
seqtbl_offset_char(SEQ_91_PICKUP_SWORD); seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
return 1; return 1;
} }
if (a == 3) { /* зелье — выпить (get_item) */
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_78_DRINK);
return 1;
}
return 0; return 0;
} }
@@ -125,13 +139,14 @@ static void run_jump(void)
{ {
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это /* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
* полировка K3; K2b просто запускает run-jump. */ * полировка K3; K2b просто запускает run-jump. */
if (Kid.frame >= FRAME_7_RUN) { if (Char.frame >= FRAME_7_RUN) {
control_up = release_arrows(); control_up = release_arrows();
seqtbl_offset_char(SEQ_4_RUN_JUMP); seqtbl_offset_char(SEQ_4_RUN_JUMP);
} }
} }
static void safe_step(void); /* fwd */ static void safe_step(void); /* fwd */
static void draw_sword(void); /* fwd: нужен в control_standing */
static void forward_pressed(void) static void forward_pressed(void)
{ {
@@ -190,15 +205,15 @@ static void safe_step(void)
control_forward = CONTROL_IGNORE; control_forward = CONTROL_IGNORE;
distance = pop_edge_distance(); distance = pop_edge_distance();
if (distance) { if (distance) {
Kid.repeat = 1; Char.repeat = 1;
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE)); seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
} else if (pop_edge_type() == EDGE_WALL) { } else if (pop_edge_type() == EDGE_WALL) {
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад /* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */ * (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
Kid.repeat = 1; Char.repeat = 1;
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE)); seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
} else if (Kid.repeat != 0) { } else if (Char.repeat != 0) {
Kid.repeat = 0; Char.repeat = 0;
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE); seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
} else { } else {
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */ seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
@@ -225,7 +240,32 @@ static void control_standing(void)
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD && if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
get_item_action()) get_item_action())
return; return;
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */ /* Ветку «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_shift == CONTROL_HELD) {
if (control_backward == CONTROL_HELD) back_pressed(); if (control_backward == CONTROL_HELD) back_pressed();
else if (control_up == CONTROL_HELD) up_pressed(); else if (control_up == CONTROL_HELD) up_pressed();
@@ -266,7 +306,7 @@ static void control_running(void)
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок /* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
* seq_47 при беге в стену (как оригинал), с корректной позицией. */ * seq_47 при беге в стену (как оригинал), с корректной позицией. */
if (control_x == CONTROL_RELEASED && 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(); control_forward = release_arrows();
seqtbl_offset_char(SEQ_13_STOP_RUN); seqtbl_offset_char(SEQ_13_STOP_RUN);
} else if (control_x == CONTROL_HELD_BACKWARD) { } else if (control_x == CONTROL_HELD_BACKWARD) {
@@ -317,21 +357,216 @@ static void control_jumpup(void)
standing_jump(); standing_jump();
} }
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */ /* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
static void control(void) * клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
* уже как выпад. */
static void draw_sword(void)
{ {
uint8_t f = Kid.frame; 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)
{
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) и в свободном падении управление /* seg005 control(): в отскоке (bumped) и в свободном падении управление
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок * НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и * прерывает присед после удара (medland/плита по голове) нажатием ↑, и
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие * последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на * стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
* пару пикселей меньше, чем должен»). */ * пару пикселей меньше, чем должен»). */
if (Kid.action == 5 || Kid.action == 4) { /* bumped / in_freefall */ if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
release_arrows(); release_arrows();
return; return;
} }
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53)) /* Порядок веток — как в 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(); control_standing();
else if (f == FRAME_48_TURN) else if (f == FRAME_48_TURN)
control_turning(); control_turning();
@@ -406,12 +641,12 @@ static void flip_control_x(void)
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */ /* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
static void user_control(void) static void user_control(void)
{ {
if (Kid.direction >= DIR_RIGHT) { if (Char.direction >= DIR_RIGHT) {
flip_control_x(); flip_control_x();
control(); pop_control();
flip_control_x(); flip_control_x();
} else { } else {
control(); pop_control();
} }
} }
@@ -451,6 +686,12 @@ void pop_ctrl_tick(void)
rest_ctrl_1(); rest_ctrl_1();
read_input(); read_input();
read_user_control(); read_user_control();
user_control(); /* -> control(): может сменить Kid.curr_seq */ /* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
* персонажем (как в оригинале), поэтому ввод игрока применяется к
* Киду только внутри loadkid/savekid. Кадр не перезагружаем —
* play_seq в kid_tick сделает это следующим шагом. */
pop_loadkid();
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
pop_savekid_state();
save_ctrl_1(); save_ctrl_1();
} }
+23
View File
@@ -32,4 +32,27 @@ void pop_ctrl_tick(void);
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */ * зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
int8_t pop_ctrl_shift_held(void); int8_t pop_ctrl_shift_held(void);
/* ---- Шина синтетического ввода (порт глобалов 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. */
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);
#endif #endif
+223
View File
@@ -0,0 +1,223 @@
/*
* 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_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];
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;
}
void pop_guard_heal(void) __banked
{
uint8_t p = gfx_get_draw_page() & 1;
if (gvalid[p]) gfx_heal(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
if (svalid[p]) gfx_heal(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;
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);
}
/* ---- Индикаторы 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();
}
}
+217 -4
View File
@@ -15,9 +15,222 @@ int8_t pop_y_to_row(int16_t y)
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1); return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
} }
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv) /* Уложить значение в диапазон 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)
{ {
unsigned long s = *seed * 214013UL + 2531011UL; uint16_t n = (uint16_t)(maxv + 1);
*seed = s; if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
return (uint16_t)((uint16_t)(s >> 16) % (uint16_t)(maxv + 1)); 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
+31 -4
View File
@@ -26,9 +26,36 @@ extern const int16_t pop_y_land[5];
* над комнатой, 3 = под комнатой). */ * над комнатой, 3 = под комнатой). */
int8_t pop_y_to_row(int16_t y); int8_t pop_y_to_row(int16_t y);
/* prandom (seg009:321) — 32-битный LCG оригинала. Сид ВНЕШНИЙ: у стен /* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
* (раскладка кладки) и у анимаций тайлов (фаза факелов) свои * анимаций тайлов (фаза факелов) свои последовательности, смешивать их
* последовательности, смешивать их нельзя. */ * нельзя.
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv); *
* ДВА генератора, выбор через 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 #endif
+164
View File
@@ -0,0 +1,164 @@
/*
* 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));
}
void pop_guard_enter(uint8_t room)
{
uint8_t tile, color, skill;
int8_t dir;
pop_guard_reset();
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[уровень].
* Уровень 1 -> 3; extrastrength ненулевой только у skill 4. */
guardhp_max = guardhp_curr = (uint8_t)(3 + (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);
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;
}
}
}
+164
View File
@@ -0,0 +1,164 @@
/*
* 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;
/* Убрать стража со сцены (смена комнаты / инициализация). */
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
+331 -98
View File
@@ -10,12 +10,13 @@
#include <gfx.h> #include <gfx.h>
#include <sprite.h> #include <sprite.h>
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ #include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */ #include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h" #include "pop_geom.h"
#include "kid_data.h" #include "kid_data.h"
kid_t Kid; pop_char_t Kid;
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN /* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */ * в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
@@ -27,33 +28,99 @@ int8_t knock;
static atlas_t kidp[KID_MAXPAGES]; static atlas_t kidp[KID_MAXPAGES];
static uint8_t kid_npages; static uint8_t kid_npages;
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): отдельный /* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237). * ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
* Из sword_tbl взяты только строки 35..42 — те, что адресуют эти кадры (бой * Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
* не портирован); индекс в атласе = sword_frame − 35. Смещения таблицы — * sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */ * Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
#define SWORD_SF_LO 35 * calc_screen_x_coord, x — «вперёд» по направлению персонажа. */
#define SWORD_SF_HI 42 static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 }; static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 }; static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
static atlas_t swordp; static atlas_t swordp;
static uint8_t sword_ok; static uint8_t sword_ok;
int pop_sword_load(void) int pop_sword_load(void)
{ {
if (atlas_load(&swordp, "sword.atl") != 0) return -1; if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
sword_ok = 1; sword_ok = 1;
return 0; 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 выдал бы марку транспонированной. */
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
gfx_w0_unmap();
}
int pop_kid_load(uint8_t npages) int pop_kid_load(uint8_t npages)
{ {
static const char *const names[KID_MAXPAGES] = { static const char *const names[KID_MAXPAGES] = {
"kid0.atl","kid1.atl","kid2.atl","kid3.atl","kid4.atl","kid5.atl", "KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
"kid6.atl","kid7.atl","kid8.atl","kid9.atl","kid10.atl","kid11.atl", "KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
"kid12.atl","kid13.atl","kid14.atl","kid15.atl","kid16.atl","kid17.atl", "KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
"kid18.atl","kid19.atl","kid20.atl","kid21.atl","kid22.atl","kid23.atl", "KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
"kid24.atl","kid25.atl","kid26.atl","kid27.atl" "KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
}; };
uint8_t i; uint8_t i;
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES; if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
@@ -98,6 +165,16 @@ static const uint8_t *kd_frame_ptr(uint8_t i)
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF); off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
return (const uint8_t *)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) + \ #define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
(uint16_t)((cs) - SEQTBL_BASE))) (uint16_t)((cs) - SEQTBL_BASE)))
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной /* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
@@ -106,8 +183,76 @@ static const uint8_t *kd_frame_ptr(uint8_t i)
static uint8_t kdat_blk, kdat_page, kdat_ok; static uint8_t kdat_blk, kdat_page, kdat_ok;
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы. */ /* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
* тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
static kframe cur_frame; 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) int pop_kid_data_load(const char *path)
{ {
@@ -145,22 +290,60 @@ void pop_kid_data_free(void)
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; } 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 из таблицы в /* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */ * cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
static void load_frame(void) static void load_frame(void)
{ {
const uint8_t *f = kd_frame_ptr(Kid.frame); /* Таблица кадров зависит от персонажа (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.image = f[0];
cur_frame.dx = (int8_t)f[1]; cur_frame.dx = (int8_t)f[1];
cur_frame.dy = (int8_t)f[2]; cur_frame.dy = (int8_t)f[2];
cur_frame.flags = f[3]; cur_frame.flags = f[3];
cur_frame.sword = f[4]; 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) static uint8_t char_dx_forward(int8_t dx)
{ {
if (Kid.direction < 0) dx = (int8_t)-dx; if (Char.direction < 0) dx = (int8_t)-dx;
return (uint8_t)(Kid.x + dx); return (uint8_t)(Char.x + dx);
} }
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу — /* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
@@ -172,6 +355,11 @@ uint8_t kid_cur_flags(void) { return cur_frame.flags; }
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */ * 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]; } void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
/* То же для АКТИВНОГО персонажа — внутри окна 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). /* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов * Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце — * (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
@@ -181,42 +369,42 @@ void play_seq(void)
if (!kdat_ok) return; if (!kdat_ok) return;
gfx_w0_map(kdat_page); gfx_w0_map(kdat_page);
for (;;) { for (;;) {
uint8_t cmd = SEQ(Kid.curr_seq); uint8_t cmd = SEQ(Char.curr_seq);
Kid.curr_seq++; Char.curr_seq++;
switch (cmd) { switch (cmd) {
case 0xFB: /* DX */ case 0xFB: /* DX */
Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq)); Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xFA: /* DY */ case 0xFA: /* DY */
Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq)); Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xFE: /* FLIP */ case 0xFE: /* FLIP */
Kid.direction = (int8_t)~Kid.direction; Char.direction = (int8_t)~Char.direction;
break; break;
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */ case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
Kid.curr_seq += 2; Char.curr_seq += 2;
break; break;
case 0xFF: { /* JMP */ case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) | uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8)); ((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
Kid.curr_seq = a; Char.curr_seq = a;
break; break;
} }
case 0xFD: /* UP */ case 0xFD: /* UP */
Kid.curr_row--; Char.curr_row--;
break; break;
case 0xFC: /* DOWN */ case 0xFC: /* DOWN */
Kid.curr_row++; Char.curr_row++;
break; break;
case 0xF9: /* ACTION */ case 0xF9: /* ACTION */
Kid.action = SEQ(Kid.curr_seq); Char.action = SEQ(Char.curr_seq);
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xF8: /* SET_FALL */ case 0xF8: /* SET_FALL */
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++; Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++; Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
break; break;
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */ case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
knock = 1; knock = 1;
@@ -225,20 +413,20 @@ void play_seq(void)
knock = -1; knock = -1;
break; break;
case 0xF2: /* SOUND — потребить 1 байт */ case 0xF2: /* SOUND — потребить 1 байт */
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xF1: /* END_LEVEL */ case 0xF1: /* END_LEVEL */
break; break;
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
* (порт play_seq -> proc_get_object, seg006) */ * (порт play_seq -> proc_get_object, seg006) */
if (SEQ(Kid.curr_seq) == 1) if (SEQ(Char.curr_seq) == 1)
pop_proc_get_object(); pop_proc_get_object();
Kid.curr_seq++; Char.curr_seq++;
break; break;
case 0xF6: /* DIE nop */ case 0xF6: /* DIE nop */
break; break;
default: /* КАДР */ default: /* КАДР */
Kid.frame = cmd; Char.frame = cmd;
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */ load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
gfx_w0_unmap(); gfx_w0_unmap();
return; return;
@@ -246,6 +434,12 @@ void play_seq(void)
} }
} }
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
* 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) void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
{ {
Kid.curr_col = col; Kid.curr_col = col;
@@ -255,13 +449,15 @@ void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
Kid.y = (uint8_t)pop_y_land[row + 1]; Kid.y = (uint8_t)pop_y_land[row + 1];
Kid.fall_x = Kid.fall_y = 0; Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0; Kid.repeat = 0;
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
Kid.curr_seq = kid_seq_off[seq_id]; Kid.curr_seq = kid_seq_off[seq_id];
play_seq(); /* до первого кадра */ pop_kid_play(); /* до первого кадра */
} }
void kid_tick(void) void kid_tick(void)
{ {
play_seq(); pop_kid_play();
} }
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy /* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
@@ -291,6 +487,64 @@ static int kid_lx[2], kid_ly[2];
static uint16_t kid_lw[2], kid_lh[2]; static uint16_t kid_lw[2], kid_lh[2];
static uint8_t kid_lvalid[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): /* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */ * obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
static int kid_fp_x, kid_fp_y; static int kid_fp_x, kid_fp_y;
@@ -334,23 +588,18 @@ void kid_draw_splash(void)
int top = obj_y - (int)h + 1; int top = obj_y - (int)h + 1;
if (flip) bx -= (int)w; if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols(bx, top + POP_YOFF, img, flip); 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); gfx_set_bank(GFX_BANK_NORMAL);
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе /* Брызги — накладной спрайт: свой прямоугольник heal (см.
* брызги останутся на экране после кадра. */ * kid_overlay_add), окно fore пересчитываем объединением. */
{ {
uint8_t dp = gfx_get_draw_page(); uint8_t dp = gfx_get_draw_page();
if (kid_lvalid[dp]) { kid_overlay_add(dp, bx, top, w, h);
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx; kid_fore_clip(dp);
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
int x1 = kid_lx[dp] + (int)kid_lw[dp];
int y1 = kid_ly[dp] + (int)kid_lh[dp];
if (bx + (int)w > x1) x1 = bx + (int)w;
if (top + (int)h > y1) y1 = top + (int)h;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
}
} }
} }
gfx_w0_unmap(); gfx_w0_unmap();
@@ -362,17 +611,23 @@ void kid_heal(void)
uint8_t p = gfx_get_draw_page(); uint8_t p = gfx_get_draw_page();
if (kid_lvalid[p]) if (kid_lvalid[p])
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */ gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
kid_lvalid[p] = 0; if (kid_ovalid[p])
gfx_heal(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) void kid_draw(void)
{ {
kframe fr = cur_frame; /* load_frame уже положил кадр (play_seq) */ kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */
int fwd, obj_x, obj_y, top; int fwd, obj_x, obj_y, top;
uint8_t page, idx; uint8_t page, idx;
const uint8_t *img; const uint8_t *img;
uint16_t w, h; uint16_t w, h;
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
* кадра / страница не загружена) перекрывать нечего, и fore-проход
* не должен трогать экран. */
pop_fore_set_clip(0, 0, 0, 0);
if (fr.image == 255) if (fr.image == 255)
return; /* пустой кадр */ return; /* пустой кадр */
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠ /* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
@@ -417,8 +672,14 @@ void kid_draw(void)
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */ if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
} }
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
if (skip < (int)h) /* +YOFF: центрирование */ if (skip < (int)h) { /* +YOFF: центрирование */
gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0); if (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(bx, top + skip + POP_YOFF, img, flip, skip, 0);
}
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) — /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
* узко, только по прямоугольнику Кида и только при выходе. */ * узко, только по прямоугольнику Кида и только при выходе. */
@@ -431,44 +692,16 @@ void kid_draw(void)
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */ kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
} }
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
* Kid. Прямоугольник heal расширяем объединением — иначе клинок, выйдя * Kid — со СВОИМ прямоугольником heal (kid_overlay_add). */
* за габарит Kid, оставит след. */ {
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) { int sx, sy; uint16_t sw, sh;
uint8_t sf = (uint8_t)(fr.sword & 0x3F); if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) { kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
uint16_t sw, sh;
gfx_w0_map(swordp.page);
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (sw && sh) {
uint8_t sflip = (Kid.direction >= 0);
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
: (int)SWORD_DX[si]);
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
uint8_t dp = gfx_get_draw_page() & 1;
if (sflip) sbx -= (int)sw;
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
if (!kid_lvalid[dp]) {
kid_lx[dp] = sbx; kid_ly[dp] = stop;
kid_lw[dp] = sw; kid_lh[dp] = sh;
kid_lvalid[dp] = 1;
} else {
int x0 = kid_lx[dp], y0 = kid_ly[dp];
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
if (sbx < x0) x0 = sbx;
if (stop < y0) y0 = stop;
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
if (stop + (int)sh > y1) y1 = stop + (int)sh;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0);
kid_lh[dp] = (uint16_t)(y1 - y0);
}
}
}
} }
gfx_w0_unmap(); gfx_w0_unmap();
/* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
* fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
* тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
* нулевое окно = fore-проход не трогает экран, что и правильно. */
kid_fore_clip(gfx_get_draw_page() & 1);
} }
+50 -14
View File
@@ -11,20 +11,11 @@
#define POP_KID_H #define POP_KID_H
#include <stdint.h> #include <stdint.h>
#include "pop_char.h"
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
/* Состояние персонажа (подмножество char_type, types.h). */ /* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
typedef struct { extern pop_char_t Kid;
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;
extern kid_t Kid;
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN /* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */ * (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
@@ -39,6 +30,11 @@ int pop_kid_load(uint8_t npages);
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт). * первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
* Страница маппится в W0 внутри play_seq, раз за тик. */ * Страница маппится в W0 внутри play_seq, раз за тик. */
int pop_kid_data_load(const char *path); 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); void pop_kid_data_free(void);
/* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах /* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах
@@ -48,6 +44,15 @@ void pop_kid_data_free(void);
int pop_sword_load(void); int pop_sword_load(void);
void pop_kid_free(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, /* Инициализировать Kid: последовательность seq_id, клетка col/row,
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */ * направление 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); void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
@@ -58,7 +63,34 @@ void kid_tick(void);
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же /* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
* physics-шаг, как оригинал). */ * 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);
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования /* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
* таблицы во втором TU). */ * таблицы во втором TU). */
@@ -85,6 +117,10 @@ void kid_draw(void);
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
void kid_draw_splash(void); 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 —
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */ * вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
void kid_heal(void); void kid_heal(void);
+108
View File
@@ -23,10 +23,22 @@ extern void _gfx_w0_isr(void);
#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */ #define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */
#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */ #define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */
#define BP_LINKS 1952u #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 BP_START 2112u
#define ROOM_TILES 30u #define ROOM_TILES 30u
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */ #define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
/* fwd: инициализация живой копии стражей (тело ниже). */
static void gstate_init(void);
static uint8_t lvl_blk, lvl_page, lvl_ok; static uint8_t lvl_blk, lvl_page, lvl_ok;
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */ /* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
static uint8_t dl1[256], dl2[256]; static uint8_t dl1[256], dl2[256];
@@ -68,6 +80,7 @@ int pop_level_load(const char *path)
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; } if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
lvl_ok = 1; lvl_ok = 1;
gstate_init(); /* живая копия состояния стражей — из уровня */
return 0; return 0;
} }
@@ -234,6 +247,101 @@ static uint8_t start_byte(uint16_t off)
return v; 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_room(void) { return start_byte(BP_START + 0); }
uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); } 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); } int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); }
+17
View File
@@ -73,6 +73,23 @@ void pop_doorlink2_set(uint8_t i, uint8_t v);
int8_t pop_room_col_landing(uint8_t room, uint8_t col); int8_t pop_room_col_landing(uint8_t room, uint8_t col);
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */ /* Стартовая позиция уровня (блок @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_room(void);
uint8_t pop_level_start_pos(void); uint8_t pop_level_start_pos(void);
int8_t pop_level_start_dir(void); int8_t pop_level_start_dir(void);
+84 -34
View File
@@ -28,6 +28,7 @@
* есть то, что понадобится стражам, чиста от графики. Разделение mob на * есть то, что понадобится стражам, чиста от графики. Разделение mob на
* логику и отрисовку — задел следующей фазы. */ * логику и отрисовку — задел следующей фазы. */
#include "pop_bg.h" #include "pop_bg.h"
#include "pop_state.h"
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */ #include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
@@ -113,6 +114,12 @@ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавше
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */ uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */ uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
/* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки
* (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале.
* Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и
* проверен до боёвки, и переводить его на дельту без нужды рискованно. */
int8_t hitp_delta;
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */ static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
@@ -149,13 +156,13 @@ uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провал
uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */ uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */
void pop_map_set_above(uint8_t *above_row2_fg) { g_above = above_row2_fg; } void pop_map_set_above(uint8_t *above_row2_fg) __banked { g_above = above_row2_fg; }
void pop_map_set(uint8_t *fg) { g_fg = fg; } void pop_map_set(uint8_t *fg) __banked { g_fg = fg; }
void pop_map_set_room(uint8_t room) { g_room = room; } void pop_map_set_room(uint8_t room) __banked { g_room = room; }
void pop_kid_hp_reset(void) { hitp_curr = 3; pop_kid_dead = 0; } void pop_kid_hp_reset(void) __banked { hitp_curr = hitp_max = 3; hitp_delta = 0; pop_kid_dead = 0; }
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */ /* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
static uint8_t take_hp(uint8_t count) static uint8_t take_hp(uint8_t count)
@@ -166,7 +173,7 @@ static uint8_t take_hp(uint8_t count)
} }
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, 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) const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked
{ {
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down; g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; } if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; }
@@ -201,6 +208,16 @@ static uint8_t get_tile(int8_t col, int8_t row)
return g_curr_tile; return g_curr_tile;
} }
/* Тайл ТЕКУЩЕЙ комнаты по клетке — наружу (порт get_tile(room,col,row) для
* случая room == текущая). Нужен лучу видимости стража
* (check_can_guard_see_kid, seg003:688): тот идёт по ряду между Кидом и
* стражем, а оба по условию в одной (= отрисованной) комнате. Колонки за
* пределами 0..9 get_tile разрешает через швы — здесь не используются. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked
{
return get_tile(col, row);
}
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и /* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто * идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в * проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
@@ -441,7 +458,7 @@ static void land(void)
if (deadly || take_hp(1)) { /* 3+ этажа или последнее HP */ if (deadly || take_hp(1)) { /* 3+ этажа или последнее HP */
take_hp(100); take_hp(100);
kid_set_seq(SEQ_22_CRUSHED); kid_set_seq(SEQ_22_CRUSHED);
play_seq(); pop_kid_play();
determine_col(); determine_col();
pop_kid_dead = 1; pop_kid_dead = 1;
return; return;
@@ -450,7 +467,7 @@ static void land(void)
} }
Kid.fall_x = Kid.fall_y = 0; Kid.fall_x = Kid.fall_y = 0;
kid_set_seq(seq); kid_set_seq(seq);
play_seq(); pop_kid_play();
determine_col(); determine_col();
} }
@@ -464,7 +481,7 @@ static void start_fall(void)
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP; else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */ else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
kid_set_seq(seq); kid_set_seq(seq);
play_seq(); pop_kid_play();
determine_col(); determine_col();
/* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2). /* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2).
* Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose) * Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose)
@@ -530,7 +547,7 @@ static void check_grab(void)
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
Kid.fall_y = 0; Kid.fall_y = 0;
kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR); kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
play_seq(); pop_kid_play();
determine_col(); determine_col();
grab_timer = 12; /* (TODO: sound grab) */ grab_timer = 12; /* (TODO: sound grab) */
} }
@@ -587,7 +604,7 @@ static int edge_classify(int d)
return d; return d;
} }
int pop_edge_distance(void) int pop_edge_distance(void) __banked
{ {
uint8_t t; int d; uint8_t t; int d;
determine_col(); determine_col();
@@ -629,7 +646,7 @@ int pop_edge_distance(void)
return distance_to_edge_weight(); return distance_to_edge_weight();
} }
uint8_t pop_edge_type(void) { return g_edge_type; } uint8_t pop_edge_type(void) __banked { return g_edge_type; }
/* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- * /* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- *
* pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в * pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в
@@ -682,7 +699,7 @@ static uint8_t jump_up_or_grab(void)
return grab_up_with_floor_behind(); return grab_up_with_floor_behind();
} }
uint8_t pop_jump_up_seq(void) uint8_t pop_jump_up_seq(void) __banked
{ {
uint8_t through = get_tile_above_char(); uint8_t through = get_tile_above_char();
get_tile_front_above_char(); /* g_curr_tile = спереди-сверху */ get_tile_front_above_char(); /* g_curr_tile = спереди-сверху */
@@ -697,7 +714,7 @@ uint8_t pop_jump_up_seq(void)
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига — /* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */ * позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
int pop_wall_ahead(void) int pop_wall_ahead(void) __banked
{ {
int d = pop_edge_distance(); int d = pop_edge_distance();
return (g_edge_type == EDGE_WALL && d <= 2); return (g_edge_type == EDGE_WALL && d <= 2);
@@ -708,7 +725,7 @@ int pop_wall_ahead(void)
* тайловые предикаты + выбор seq (+ выравнивание Kid.x). */ * тайловые предикаты + выбор seq (+ выравнивание Kid.x). */
/* grab_timer истёк — можно подтягиваться? */ /* grab_timer истёк — можно подтягиваться? */
uint8_t pop_hang_grab_ready(void) { return grab_timer == 0; } uint8_t pop_hang_grab_ready(void) __banked { return grab_timer == 0; }
/* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10) /* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10)
* либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал * либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал
@@ -716,7 +733,7 @@ uint8_t pop_hang_grab_ready(void) { return grab_timer == 0; }
* curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6 * curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6
* — под поднятой решёткой (modif >> 2 >= 6) подъём обычный. Без проверки * — под поднятой решёткой (modif >> 2 >= 6) подъём обычный. Без проверки
* модификатора Kid срывался и с открытых ворот. */ * модификатора Kid срывался и с открытых ворот. */
uint8_t pop_hang_climb_up_seq(void) uint8_t pop_hang_climb_up_seq(void) __banked
{ {
uint8_t t = get_tile_above_char(); uint8_t t = get_tile_above_char();
if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) || if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) ||
@@ -728,7 +745,7 @@ uint8_t pop_hang_climb_up_seq(void)
/* Shift в висе: 1 = висит у стены/двери (caller играет seq_25); -1 = /* Shift в висе: 1 = висит у стены/двери (caller играет seq_25); -1 =
* сорваться (hang_fall); 0 = сверху пол, остаётся висеть (ничего). */ * сорваться (hang_fall); 0 = сверху пол, остаётся висеть (ничего). */
int8_t pop_hang_wall(void) int8_t pop_hang_wall(void) __banked
{ {
uint8_t t = get_tile_at_char(); uint8_t t = get_tile_at_char();
if (Kid.action != ACT_HANGSTRAIGHT && if (Kid.action != ACT_HANGSTRAIGHT &&
@@ -743,7 +760,7 @@ int8_t pop_hang_wall(void)
/* hang_fall (seg005:0A46): сорваться. Нет пола ни сзади, ни под /* hang_fall (seg005:0A46): сорваться. Нет пола ни сзади, ни под
* персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7) * персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7)
* и seq_11 (встать, конец спуска). Меняет Kid.x, возвращает seq. */ * и seq_11 (встать, конец спуска). Меняет Kid.x, возвращает seq. */
uint8_t pop_hang_fall_seq(void) uint8_t pop_hang_fall_seq(void) __banked
{ {
uint8_t behind = get_tile_behind_char(); uint8_t behind = get_tile_behind_char();
uint8_t at; uint8_t at;
@@ -760,7 +777,7 @@ uint8_t pop_hang_fall_seq(void)
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 = * либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
* приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки * приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки
* пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */ * пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */
uint8_t pop_down_action(void) uint8_t pop_down_action(void) __banked
{ {
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) { if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
Kid.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */ Kid.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */
@@ -792,6 +809,9 @@ uint8_t pop_down_action(void)
uint8_t pop_have_sword; uint8_t pop_have_sword;
uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */ uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */ uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
* flash_color; нам хватает двух значений, которые реально встречаются. */
uint8_t pop_flash_red;
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */ static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить /* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
@@ -805,7 +825,7 @@ static void do_pickup(int8_t obj_type, uint8_t tilepos)
/* proc_get_object (seg006:16CB): эффект поднятого предмета. Зовётся из /* proc_get_object (seg006:16CB): эффект поднятого предмета. Зовётся из
* play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). */ * play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). */
void pop_proc_get_object(void) void pop_proc_get_object(void) __banked
{ {
if (pickup_obj_type == -1) { if (pickup_obj_type == -1) {
pop_have_sword = 1; /* в оригинале have_sword = -1 */ pop_have_sword = 1; /* в оригинале have_sword = -1 */
@@ -815,13 +835,35 @@ void pop_proc_get_object(void)
* чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */ * чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */
pop_flash_time = 8; pop_flash_time = 8;
} }
pickup_obj_type = 0; /* зелья (типы 1..6) не портированы */ else {
/* proc_get_object (seg006:1927), ветка зелий. Типы 3/4/6
* (перо/переворот/открыть все ворота) — свойства поздних уровней,
* их портируем вместе с этими уровнями. */
switch (pickup_obj_type) {
case 1: /* здоровье: +1 HP, красная вспышка */
if (hitp_curr != hitp_max) {
hitp_delta = 1;
pop_flash_red = 1;
pop_flash_time = 2;
}
break;
case 5: /* «злое» зелье: −1 HP */
hitp_delta = -1;
pop_flash_red = 1;
pop_flash_time = 2;
break;
default: /* 2 жизнь / 3 перо / 4 переворот /
* 6 открыть — TODO с их уровнями */
break;
}
}
pickup_obj_type = 0;
} }
/* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет /* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет
* (управление идёт обычной веткой); 1 — присесть (caller зовёт crouch()); * (управление идёт обычной веткой); 1 — присесть (caller зовёт crouch());
* 2 — поднять меч (caller ставит seq_91). */ * 2 — поднять меч (caller ставит seq_91). */
uint8_t pop_get_item_action(void) uint8_t pop_get_item_action(void) __banked
{ {
uint8_t t; uint8_t t;
int8_t icol; int8_t icol;
@@ -844,9 +886,17 @@ uint8_t pop_get_item_action(void)
Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2)); Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
return 1; return 1;
} }
if (t != TILE_SWORD) return 0; /* питьё зелий не портировано */ {
do_pickup(-1, (uint8_t)(Kid.curr_row * 10 + icol)); uint8_t tp = (uint8_t)(Kid.curr_row * 10 + icol);
return 2; if (t == TILE_SWORD) {
do_pickup(-1, tp);
return 2; /* поднять меч (seq_91) */
}
/* Зелье: тип лежит в СТАРШИХ битах модификатора (get_item,
* seg005: do_pickup(curr_room_modif[curr_tilepos] >> 3)). */
do_pickup((int8_t)(pop_trob_modif(g_room)[tp] >> 3), tp);
return 3; /* пить (seq_78) */
}
} }
/* ---- Диспетчер ----------------------------------------------------- */ /* ---- Диспетчер ----------------------------------------------------- */
@@ -885,7 +935,7 @@ static void bumped_fall(void)
Kid.fall_x = 0; Kid.fall_x = 0;
} else { } else {
kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */ kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */
play_seq(); pop_kid_play();
} }
} }
@@ -919,7 +969,7 @@ static void bumped_floor(void)
kid_set_seq(SEQ_46_HARDBUMP); kid_set_seq(SEQ_46_HARDBUMP);
else else
kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */ kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */
play_seq(); pop_kid_play();
} }
/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять /* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять
@@ -1113,7 +1163,7 @@ static void do_knock(int tile_row)
/* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl- /* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl-
* командах KNOCK_UP/DOWN) тряхнуть ряд. KNOCK_DOWN (knock=-1, приземление) → * командах KNOCK_UP/DOWN) тряхнуть ряд. KNOCK_DOWN (knock=-1, приземление) →
* ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */ * ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */
void pop_check_knock(void) void pop_check_knock(void) __banked
{ {
if (knock) { if (knock) {
do_knock(Kid.curr_row - (knock > 0)); do_knock(Kid.curr_row - (knock > 0));
@@ -1124,7 +1174,7 @@ void pop_check_knock(void)
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest /* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
* индексируются позицией тайла и не должны утечь из старой комнаты в новую. * индексируются позицией тайла и не должны утечь из старой комнаты в новую.
* Плюс останавливает падающий кусок (pop_bg). */ * Плюс останавливает падающий кусок (pop_bg). */
void pop_loose_reset(void) void pop_loose_reset(void) __banked
{ {
int i; int i;
for (i = 0; i < 30; i++) { for (i = 0; i < 30; i++) {
@@ -1156,14 +1206,14 @@ static void fell_on_your_head(void)
pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */ pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */
if (take_hp(1)) { if (take_hp(1)) {
kid_set_seq(SEQ_22_CRUSHED); kid_set_seq(SEQ_22_CRUSHED);
play_seq(); pop_kid_play();
determine_col(); determine_col();
pop_kid_dead = 1; pop_kid_dead = 1;
} else if (frame != FRAME_109_CROUCH) { } else if (frame != FRAME_109_CROUCH) {
if (get_tile_behind_char() == TILE_EMPTY) if (get_tile_behind_char() == TILE_EMPTY)
Kid.x = (uint8_t)char_dx_forward(-2); Kid.x = (uint8_t)char_dx_forward(-2);
kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID); kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
play_seq(); pop_kid_play();
determine_col(); determine_col();
} }
} }
@@ -1183,7 +1233,7 @@ static void check_loose_fall_on_kid(void)
/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ /* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ
* (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */ * (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */
void pop_loose_tick(void) void pop_loose_tick(void) __banked
{ {
int pos; int pos;
/* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80 /* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80
@@ -1280,7 +1330,7 @@ void pop_loose_tick(void)
* check_spike_below колонки берутся get_tile_div_mod БЕЗ 7. * check_spike_below колонки берутся get_tile_div_mod БЕЗ 7.
* *
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */ * Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
{ {
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 }; static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
int wh = ((int)w + 1) >> 1; /* char_width_half */ int wh = ((int)w + 1) >> 1; /* char_width_half */
@@ -1429,7 +1479,7 @@ static void spiked(uint8_t tilepos)
Kid.fall_y = 0; Kid.fall_y = 0;
take_hp(100); /* (TODO: sound_48_spiked) */ take_hp(100); /* (TODO: sound_48_spiked) */
kid_set_seq(SEQ_51_SPIKED); kid_set_seq(SEQ_51_SPIKED);
play_seq(); pop_kid_play();
determine_col(); determine_col();
pop_kid_dead = 1; pop_kid_dead = 1;
} }
@@ -1481,7 +1531,7 @@ static void check_spike_below(void)
} }
} }
void pop_phys_tick(void) void pop_phys_tick(void) __banked
{ {
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */ if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
if (grab_timer) grab_timer--; if (grab_timer) grab_timer--;
+30 -21
View File
@@ -17,25 +17,32 @@
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив * col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию, * МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
* что и в pop_room_draw, чтобы отрисовка видела изменения. */ * что и в pop_room_draw, чтобы отрисовка видела изменения. */
void pop_map_set(uint8_t *fg); void pop_map_set(uint8_t *fg) __banked;
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии: /* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2) * lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая * в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */ * колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, 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); const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked;
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob). /* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
* Звать в enter_room. */ * Звать в enter_room. */
void pop_map_set_room(uint8_t 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;
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP. /* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/ * pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
* респавн). */ * респавн). */
extern uint8_t pop_kid_dead; extern uint8_t pop_kid_dead;
extern uint8_t hitp_curr; extern uint8_t hitp_curr;
void pop_kid_hp_reset(void); extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
void pop_kid_hp_reset(void) __banked;
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object). /* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или * pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
@@ -46,8 +53,10 @@ extern uint8_t pop_item_taken;
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока /* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */ * НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
extern uint8_t pop_flash_time; extern uint8_t pop_flash_time;
uint8_t pop_get_item_action(void); /* 0 нет / 1 присесть / 2 поднять меч */ /* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
void pop_proc_get_object(void); /* эффект — из play_seq (опкод GET_ITEM) */ 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 для /* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189). * pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
@@ -66,8 +75,8 @@ extern uint8_t pop_leave_timer;
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back), * на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
* рядом с kid-отрисовкой. */ * рядом с kid-отрисовкой. */
extern uint8_t pop_loose_modif[30]; extern uint8_t pop_loose_modif[30];
void pop_loose_tick(void); void pop_loose_tick(void) __banked;
void pop_loose_reset(void); /* сброс loose-состояния при смене комнаты */ void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ). /* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */ * Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
@@ -81,7 +90,7 @@ extern uint8_t pop_loose_fell;
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности * pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты, * между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */ * pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
void pop_map_set_above(uint8_t *above_row2_fg); void pop_map_set_above(uint8_t *above_row2_fg) __banked;
extern uint8_t pop_ceil_modif[10]; extern uint8_t pop_ceil_modif[10];
extern uint8_t pop_ceil_fell; extern uint8_t pop_ceil_fell;
extern uint8_t pop_debris_at; extern uint8_t pop_debris_at;
@@ -92,7 +101,7 @@ extern uint8_t pop_kid_hurt;
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда /* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
* приземления/удара. Зовётся из pop_phys_tick. */ * приземления/удара. Зовётся из pop_phys_tick. */
void pop_check_knock(void); void pop_check_knock(void) __banked;
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру /* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр * или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
@@ -104,14 +113,14 @@ extern uint8_t pop_fell_out;
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed + /* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
* determine_col + check_action (триггер падения / do_fall / приземление). * determine_col + check_action (триггер падения / do_fall / приземление).
* Порядок как в оригинале (seg000 play_kid_frame). */ * Порядок как в оригинале (seg000 play_kid_frame). */
void pop_phys_tick(void); void pop_phys_tick(void) __banked;
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл /* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid, * над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели). * ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер. * obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */ * Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h); int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после /* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */ * pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
@@ -120,26 +129,26 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h);
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */ #define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */ /* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
int pop_edge_distance(void); int pop_edge_distance(void) __banked;
uint8_t pop_edge_type(void); uint8_t pop_edge_type(void) __banked;
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной /* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
* позиции (передний край у стены, симметрично) и возвращает 1. Для * позиции (передний край у стены, симметрично) и возвращает 1. Для
* control_running (чистый стоп у стены). */ * control_running (чистый стоп у стены). */
int pop_wall_ahead(void); int pop_wall_ahead(void) __banked;
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО /* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x. * прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
* check_jump_up/grab_up (K4.4). control_up гасит caller. */ * 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 --- * /* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в * Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
* pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */ * pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */
uint8_t pop_hang_grab_ready(void); /* grab_timer истёк — можно climb-up */ uint8_t pop_hang_grab_ready(void) __banked; /* grab_timer истёк — можно climb-up */
uint8_t pop_hang_climb_up_seq(void); /* seq_10 / seq_73 (подтягивание) */ uint8_t pop_hang_climb_up_seq(void) __banked; /* seq_10 / seq_73 (подтягивание) */
int8_t pop_hang_wall(void); /* Shift в висе: 1=у стены,-1=fall,0=держ.*/ int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
uint8_t pop_hang_fall_seq(void); /* seq_23 / seq_11 (сорваться), +Kid.x */ uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
uint8_t pop_down_action(void); /* 1=обработано(спуск/отступ), 0=присед */ uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
#endif #endif
+19
View File
@@ -0,0 +1,19 @@
/*
* pop_state.c — данные, которые читают ОБЕ стороны банковой границы.
*
* Зачем отдельный модуль: sprinter-cc собирает банковый модуль с
* --dataseg BANKn, поэтому ВСЯ его писучая память (глобалы, file-static,
* статики внутри функций) линкуется В СТРАНИЦУ БАНКА и снаружи не читается
* — проверено на .map (2026-07-30: глобал банкового модуля лёг по
* 0x0001C000, а не в _DATA). Значит любую переменную, к которой нужен
* доступ и из банка, и снаружи, надо держать в НЕбанковом модуле — здесь.
*
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
*/
#include "pop_state.h"
uint8_t pop_loose_landed;
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
* но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */
uint8_t pop_immortal;
+21
View File
@@ -0,0 +1,21 @@
/*
* 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;
#endif
+22 -7
View File
@@ -47,7 +47,7 @@ static uint8_t trob_drawn[POP_ROOMTILES];
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и /* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
* выбора следующего кадра пламени. */ * выбора следующего кадра пламени. */
static unsigned long trob_seed; static pop_rnd_t trob_seed;
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv)) #define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
@@ -345,12 +345,29 @@ uint8_t pop_trob_room_active(uint8_t room)
return 0; return 0;
} }
static uint8_t torch_tick; /* делитель скорости анимации пламени */
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 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) void pop_process_trobs(uint8_t cur_room)
{ {
uint8_t i, w = 0; uint8_t i, w = 0;
#if TORCH_ANIM_DIV > 1
torch_tick++; torch_tick++;
#endif
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы). /* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC * row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
@@ -394,11 +411,9 @@ void pop_process_trobs(uint8_t cur_room)
break; break;
case TILE_TORCH: case TILE_TORCH:
case TILE_TORCH_DEBRIS: case TILE_TORCH_DEBRIS:
/* animate_torch (seg007:03C1): кадр пламени; анимация вечная — /* animate_torch (seg007:03C1): анимация вечная — type остаётся
* type остаётся 1, пока комната отрисована. Смена кадра — через * 1, пока комната отрисована. Темп — TORCH_ANIM_DIV. */
* ЛОГИЧЕСКИЙ кадр (torch_tick): у нас кадр короче игрового тика if (room == cur_room && TORCH_ANIM_NOW)
* оригинала, и пламя «кипело» вдвое быстрее нужного. */
if (room == cur_room && (torch_tick & 1) == 0)
mod[tp] = get_torch_frame(mod[tp]); mod[tp] = get_torch_frame(mod[tp]);
break; break;
case TILE_POTION: case TILE_POTION:
+96 -16
View File
@@ -16,9 +16,47 @@
#include "pop_level.h" #include "pop_level.h"
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_redraw.h" #include "pop_redraw.h"
#include "pop_guard.h"
#include "pop_cheat.h"
__sfr __at (0xFE) io_border; __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).
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
* вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних
* уровней, чомперы, зеркало. */
const uint8_t n_banks = 4;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */ /* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2 #define SEQ_STAND 2
#define ROOM_1 1 #define ROOM_1 1
@@ -40,6 +78,7 @@ __sfr __at (0xFE) io_border;
* Убирается закомментированием ОДНОЙ строки ниже. */ * Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1 #define ROOMNAV 1
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы /* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */ * чтобы enter_room переключал комнату без длинного списка параметров. */
@@ -131,6 +170,7 @@ static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t
static void enter_room(uint8_t room) static void enter_room(uint8_t room)
{ {
uint8_t pg; uint8_t pg;
pop_guard_leave(); /* запомнить стража уходящей комнаты */
uint8_t *room_mod; uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */ apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */
@@ -158,9 +198,16 @@ static void enter_room(uint8_t room)
} }
} }
pop_map_set_room(room); pop_map_set_room(room);
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ /* Kid.room — как в оригинале: «комната персонажа». Полная модель
* (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
* стража никогда не срабатывает. */
Kid.room = room;
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
* а не по свежему backtable. Фаза P0. */ * а не по свежему backtable. Фаза P0. */
@@ -198,21 +245,25 @@ int main(void)
#ifdef ROOMNAV #ifdef ROOMNAV
uint8_t nav_prev = 0; uint8_t nav_prev = 0;
#endif #endif
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0;
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */ if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
puts("pop_bg_load failed"); puts("pop_bg_load failed");
return 1; return 1;
} }
if (pop_kid_data_load("kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */ 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"); printf("kid_data.bin load failed\n");
return 1; return 1;
} }
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */ pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed"); puts("kid atlas load failed");
return 1; return 1;
} }
if (pop_level_load("res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */ if (pop_level_load("LEVELS\\res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */
pop_level_load("a:\\res2001.bin") != 0) { pop_level_load("a:\\res2001.bin") != 0) {
puts("level load failed"); puts("level load failed");
return 1; return 1;
@@ -220,7 +271,7 @@ int main(void)
initgraph(); initgraph();
gfx_set_visible_page(0); 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_fload(0, "a:\\kid.pal");
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */ gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
@@ -254,7 +305,7 @@ int main(void)
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */ * переключ). Выключение возвращает показ на страницу 0. */
io_border = 2; /* синий - начало */ PROF(2); /* ввод + heal */
uint8_t sp = kbd_raw_down(KBD_SPACE); uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) { if (sp && !sp_prev) {
@@ -271,8 +322,25 @@ int main(void)
* стоит, можно спокойно разбирать позу Kid и окклюзию. */ * стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0; if (kbd_raw_down(0x1E)) frozen = 0;
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K — убить стража;
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза);
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
* берётся с пола (pop_have_sword ставит подбор). */
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);
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;
cheat_prev = k;
immo_prev = i;
sword_prev = s;
}
if (frozen) { if (frozen) {
io_border = 0; /* черный: ждём vsync */ PROF(0); /* чёрный: ждём vsync */
gfx_wait_vsync(); gfx_wait_vsync();
continue; continue;
} }
@@ -312,11 +380,20 @@ int main(void)
back = dbuf ? (uint8_t)(front ^ 1) : 0; back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back); gfx_set_draw_page(back);
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
pop_guard_heal(); /* и прошлый кадр стража */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */ pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика (тот же синий) */
kid_tick(); /* play_seq: следующий кадр */ kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */ pop_phys_tick(); /* K3: падение/приземление/стена */
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 пересёк нижнюю границу комнаты */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0; pop_fell_out = 0;
@@ -339,16 +416,14 @@ int main(void)
} }
} }
io_border = 4; /* зеленый */ PROF(4); /* слой фона */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */ pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */ /* — ДО Kid, чтобы Kid всегда был поверх плиты) */
io_border = 6; /* голубой */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */ pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */ /* process_trobs — рисования внутри нет) */
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */ pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */ /* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
io_border = 4; /* синий */
{ /* ворота соседа слева (room8) видны в col0 — */ { /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) { if (lft) {
@@ -365,7 +440,6 @@ int main(void)
} }
} }
} }
io_border = 2; /* зеленый */
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++; dead_frames++;
if (dead_frames > RESPAWN_SETTLE && if (dead_frames > RESPAWN_SETTLE &&
@@ -378,13 +452,11 @@ int main(void)
pop_kid_hp_reset(); pop_kid_hp_reset();
} }
} }
io_border = 6; /* голубой */
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */ if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1); uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
uint8_t fcol = (uint8_t)(fpos % 10); uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0; pop_loose_fell = 0;
io_border = 2; /* синий */
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */ add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) { if (down) {
int8_t lrow = pop_room_col_landing(down, fcol); int8_t lrow = pop_room_col_landing(down, fcol);
@@ -421,22 +493,29 @@ int main(void)
* Приоритет как в оригинале: активный flash_time > кадр урона * Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */ * (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) { if (pop_flash_time) {
flash_bg(255, 255, 85); /* color_14_brightyellow */ /* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (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--; pop_flash_time--;
if (!pop_flash_time) pop_flash_red = 0;
flash_on = 1; flash_on = 1;
} else if (pop_kid_hurt) { } else if (pop_kid_hurt) {
flash_bg(255, 85, 85); /* color_12_brightred */ flash_bg(255, 85, 85); /* color_12_brightred */
flash_on = 1; flash_on = 1;
} }
io_border = 4; /* зеленый */
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */
PROF(6); /* kid_draw (тот же циан) */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */ pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash(); kid_draw_splash();
pop_kid_hurt = 0; pop_kid_hurt = 0;
} }
io_border = 0; /* черный */ pop_hp_draw(); /* полоса HP — только при изменении */
PROF(6); /* fore поверх Kid + борта */
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(), kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action); Kid.direction, Kid.frame, Kid.action);
@@ -446,7 +525,7 @@ int main(void)
* сделан (скрытая страница), wait_vsync ловит начало кадра, * сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */ * set_visible_page переключает во время бланка (gfx.h паттерн). */
io_border = 0; /* черный: ждём vsync */ PROF(0); /* чёрный: ждём vsync */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО /* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера), * при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
@@ -491,6 +570,7 @@ int main(void)
closegraph(); closegraph();
pop_bg_free(); pop_bg_free();
pop_kid_free(); pop_kid_free();
pop_guard_free();
pop_kid_data_free(); pop_kid_data_free();
pop_level_free(); pop_level_free();
return 0; return 0;
@@ -14,7 +14,7 @@ seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_
import os import os
import re import re
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src" SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "SDLPoP", "src")
OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "poc", "res", "kid") "..", "poc", "res", "kid")
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
@@ -26,10 +26,11 @@ def read(name):
return open(os.path.join(SRC, name)).read() return open(os.path.join(SRC, name)).read()
def parse_frame_table(txt): def parse_frame_table(txt, name="frame_table_kid"):
"""frame_table_kid[] -> список (image, dx, dy, flags). Записи вида """frame_table_kid[]/frame_tbl_guard[] -> список (image, dx, dy, flags).
{ IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как OR-выражения.""" Записи вида { IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как
m = re.search(r'const frame_type frame_table_kid\[\]\s*=\s*\{(.*?)\n\};', txt, re.S) OR-выражения."""
m = re.search(r'const frame_type ' + name + r'\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
body = m.group(1) body = m.group(1)
frames = [] frames = []
for row in re.findall(r'\{([^}]*)\}', body): for row in re.findall(r'\{([^}]*)\}', body):
@@ -43,6 +44,18 @@ def parse_frame_table(txt):
return frames return frames
def parse_sword_tbl(txt):
"""sword_tbl[] (seg006:2047) -> список (id, x, y). id 255 = клинка нет.
Индекс — sword_frame (cur_frame.sword & 0x3F); один и тот же chtab_0 и у
Кида, и у стража."""
m = re.search(r'const sword_table_type sword_tbl\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
rows = []
for row in re.findall(r'\{([^}]*)\}', m.group(1)):
a, b, c = [int(p.strip(), 0) for p in row.split(',')]
rows.append((a & 0xFF, b & 0xFF, c & 0xFF))
return rows
def parse_byte_array(txt, name): def parse_byte_array(txt, name):
m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S) m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))] return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
@@ -65,8 +78,10 @@ def main():
seqc = read("seqtbl.c") seqc = read("seqtbl.c")
frames = parse_frame_table(seg006) frames = parse_frame_table(seg006)
gframes = parse_frame_table(seg006, "frame_tbl_guard")
seqtbl = parse_byte_array(seqc, "original_seqtbl") seqtbl = parse_byte_array(seqc, "original_seqtbl")
offs = parse_word_array(seqc, "original_seqtbl_offsets") offs = parse_word_array(seqc, "original_seqtbl_offsets")
swtbl = parse_sword_tbl(seg006)
with open(OUT, "w") as f: with open(OUT, "w") as f:
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n" f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
@@ -90,13 +105,32 @@ def main():
# раскладка kid_data.bin (смещения ОТ НАЧАЛА ДАННЫХ страницы) # раскладка kid_data.bin (смещения ОТ НАЧАЛА ДАННЫХ страницы)
f.write("/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */\n") f.write("/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */\n")
f.write("#define KID_BIN_FRAMES_OFF 0\n") f.write("#define KID_BIN_FRAMES_OFF 0\n")
f.write("#define KID_BIN_SEQTBL_OFF %d\n\n" % (len(frames) * 5)) f.write("#define KID_BIN_SEQTBL_OFF %d\n" % (len(frames) * 5))
f.write("/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс\n"
" * frame + add_frame - 149 (add_frame=70 для кадров 102..106). */\n")
f.write("#define KID_NGFRAMES %d\n" % len(gframes))
f.write("#define KID_BIN_GFRAMES_OFF %d\n\n" % (len(frames) * 5 + len(seqtbl)))
# seq offsets # seq offsets
f.write("#define KID_NSEQ %d\n" % len(offs)) f.write("#define KID_NSEQ %d\n" % len(offs))
f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n") f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n")
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n") f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
f.write("#endif\n")
# sword_tbl — макро-инициализаторами, а не готовыми массивами: так
# 159 байт таблицы лягут в ОДИН TU (pop_kid.c), а не в каждый, кто
# включил заголовок.
f.write("/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.\n"
" * Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.\n"
" * Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),\n"
" * x — «вперёд» по направлению персонажа. */\n")
f.write("#define KID_NSWORD %d\n" % len(swtbl))
for i, (nm, fmt) in enumerate((("ID", "0x%02X"), ("DX", "%d"), ("DY", "%d"))):
# dx/dy — знаковые: печатаем со знаком, иначе int8_t = overflow
vals = [r[i] if i == 0 else (r[i] - 256 if r[i] > 127 else r[i])
for r in swtbl]
f.write("#define KID_SWORD_%s_INIT { \\\n" % nm)
f.write(emit_c(vals, 12, fmt).replace("\n", " \\\n") + " \\\n}\n")
f.write("\n#endif\n")
# kid_data.bin: кадры (по 5 байт) + seqtbl подряд # kid_data.bin: кадры (по 5 байт) + seqtbl подряд
os.makedirs(OUT_BIN_DIR, exist_ok=True) os.makedirs(OUT_BIN_DIR, exist_ok=True)
@@ -105,7 +139,9 @@ def main():
for (img, dx, dy, fl, sw) in frames: for (img, dx, dy, fl, sw) in frames:
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF))) b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
b.write(bytes(seqtbl)) b.write(bytes(seqtbl))
print(f"kid_data.bin: {len(frames)*5 + len(seqtbl)} Б -> {binpath}") for (img, dx, dy, fl, sw) in gframes:
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
print(f"kid_data.bin: {len(frames)*5 + len(seqtbl) + len(gframes)*5} Б (кадры Kid + seqtbl + кадры стража) -> {binpath}")
print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, " print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, "
f"{len(offs)} seq-оффсетов -> {OUT}") f"{len(offs)} seq-оффсетов -> {OUT}")
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
pop_pack_guard.py — упаковщик спрайтов СТРАЖА (chtab_5_guard, база 750).
Оригинал грузит стражей из отдельного .DAT по типу уровня
(GUARD/FAT/SKEL/VIZIER/SHADOW, seg000:1092); уровень 1 — обычный «guard»,
то есть data/GUARD/res(750+id).png. Палитра — НЕ из PNG: оригинал
подменяет её из res10.bin (guard_palettes, 7 палитр по 16 цветов, 6-бит) по
level.guards_color; на уровне 1 у обоих стражей color = 2.
Выход: poc/res/guard/g0..gN.atl (по 8 спрайтов на страницу, адресация
id>>3 / id&7 — как у Kid) + группа палитры 0x90..0x9F в общий kid.pal
(его собирает pop_pack_kid.py, см. GUARD_PAL_BASE там).
"""
import os
import sys
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pop_pack_bg as B
from pop_pack_kid import transpose_cols
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD")
PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin")
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard")
PAL_BASE = 0x90 # слоты палитры Sprinter под стража
COLOR = 2 # level.guards_color обоих стражей уровня 1
SHIFT = 3 # g[id>>3], idx id&7 (спрайты крупные)
MASK = (1 << SHIFT) - 1
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
def load_guard(id):
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
Off-by-one как у Kid (см. pop_pack_kid.load_kid): load_chtab_from_file
зовётся с ресурсом 750, а images[i-1] = res(750+i), то есть images[0] =
res751. Рисование берёт get_image(chtab, obj_id+1 1) = images[image],
значит frame.image=N -> res(751+N). С res(750+N) страж стоял в позе
предыдущего кадра (замечено на frame_166_stand_inactive: image 17
рисовался как res767 — выпад — вместо res768). """
path = os.path.join(GUARD_DIR, f"res{751 + id}.png")
if not os.path.exists(path):
return None
im = Image.open(path)
if im.mode != "P":
B.die(f"res{750+id}: mode {im.mode}, ожидался P")
src = im.tobytes()
rowmajor = bytes((0xFF if v == 0 else PAL_BASE + v) for v in src)
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
def guard_palette():
"""16 цветов (r,g,b 0..63) из res10.bin для COLOR."""
b = open(PAL_BIN, "rb").read()
off = 0x30 * COLOR - 0x30
return [(b[off + i * 3], b[off + i * 3 + 1], b[off + i * 3 + 2]) for i in range(16)]
def main():
os.makedirs(OUT_DIR, exist_ok=True)
pages = {}
n = 0
for id in range(0, MAX_ID + 1):
g = load_guard(id)
if g is None:
continue
pages.setdefault(id >> SHIFT, {})[id & MASK] = g
n += 1
if not pages:
B.die("нет спрайтов стража")
total = 0
for pg in range(max(pages) + 1):
nm = f"g{pg}.atl"
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
total += sz
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б")
print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
if __name__ == "__main__":
main()
+14 -3
View File
@@ -25,7 +25,11 @@ KID_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDL
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid") OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F
SWORD_IDS = tuple(range(20, 28)) # sword_tbl для кадров 229..236 (найти-убрать) GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
# chtab_0 целиком: sword_tbl (seg006:2047) адресует id 0..33 — подъём меча,
# ножны И вся боёвка. Меч в оригинале ОДИН на всех: и Кид, и страж рисуют
# клинок из этого же chtab_0 (add_sword_to_objtable, seg006:1798).
SWORD_IDS = tuple(range(0, 34))
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ) SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
MASK = (1 << SHIFT) - 1 MASK = (1 << SHIFT) - 1
@@ -108,6 +112,14 @@ def build_palette(out_path):
for i, (r, g, bl) in enumerate(_read16(path)): for i, (r, g, bl) in enumerate(_read16(path)):
o = (base + i) * 4 o = (base + i) * 4
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0)) pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
# Палитра стража берётся НЕ из .pal, а из res10.bin (guard_palettes:
# 7 палитр по 16 цветов, 6-бит) по level.guards_color; на уровне 1 у
# обоих стражей color = 2. См. toolchain/pop_pack_guard.py.
import pop_pack_guard as G
for i, (r, g, bl) in enumerate(G.guard_palette()):
o = (GUARD_PAL_BASE + i) * 4
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
with open(out_path, "wb") as f: with open(out_path, "wb") as f:
f.write(pal) f.write(pal)
@@ -141,8 +153,7 @@ def main():
names.append(None) names.append(None)
print(f" (kid стр {pg}: пусто)") print(f" (kid стр {pg}: пусто)")
# chtab_0: меч в руке. Пакуем ТОЛЬКО кадры подъёма/ножен (sword_tbl # chtab_0: меч в руке — весь chtab (id 0..33), индекс в атласе = id.
# 35..42 -> id 20..27); индекс в атласе = id - 20 (бой не портирован).
sw = {} sw = {}
for i, id in enumerate(SWORD_IDS): for i, id in enumerate(SWORD_IDS):
w, h, pix = load_sword(id) w, h, pix = load_sword(id)
+21 -2
View File
@@ -23,6 +23,7 @@
# CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3 # CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3
# Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3 # Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3
# -I PATH additional include path (repeatable) # -I PATH additional include path (repeatable)
# -DNAME[=VAL] preprocessor define, passed to sdcc as is (repeatable)
# -L 0xADDR code load address (default: derived from --memory mode) # -L 0xADDR code load address (default: derived from --memory mode)
# -E 0xADDR entry address (default: same as -L) # -E 0xADDR entry address (default: same as -L)
# -S 0xADDR stack address (default: 0xBFFE) # -S 0xADDR stack address (default: 0xBFFE)
@@ -32,6 +33,9 @@
# --data-loc 0xN override SDCC --data-loc (default: derived from --memory) # --data-loc 0xN override SDCC --data-loc (default: derived from --memory)
# -Wl FLAG extra linker flag (repeatable) # -Wl FLAG extra linker flag (repeatable)
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked # --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
# --bank-data put bank modules' writable data INTO the bank page
# (default: it goes to the shared _DATA in W1/W2, which
# stays mapped and is visible from everywhere)
# automatically and adds -Wl-b_BANKN=0x{N}C000 # automatically and adds -Wl-b_BANKN=0x{N}C000
# --w3 FILE.c place FILE.c resident in window 3 (0xC000), called # --w3 FILE.c place FILE.c resident in window 3 (0xC000), called
# DIRECTLY (no trampoline). Repeatable. Defaults to # DIRECTLY (no trampoline). Repeatable. Defaults to
@@ -106,6 +110,8 @@ LD_EXTRA=()
MKEXE_EXTRA=() MKEXE_EXTRA=()
BANK_SPECS=() # entries like "1=engine.c" BANK_SPECS=() # entries like "1=engine.c"
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3) W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
USER_DEFS=() # -DFOO / -DFOO=bar — пробрасываются в sdcc как есть
BANK_LOCAL_DATA=0 # 1 (--bank-data): писучие данные банков — В СТРАНИЦУ банка
W3_RELS=() # заполняется при компиляции W3-модулей W3_RELS=() # заполняется при компиляции W3-модулей
W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3 W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3
USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small) USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small)
@@ -139,7 +145,9 @@ while [[ $# -gt 0 ]]; do
--w3) W3_SPECS+=("$2"); shift 2;; --w3) W3_SPECS+=("$2"); shift 2;;
--stack-size) STACK_SIZE="$2"; shift 2;; --stack-size) STACK_SIZE="$2"; shift 2;;
-Wl) LD_EXTRA+=("$2"); shift 2;; -Wl) LD_EXTRA+=("$2"); shift 2;;
-D*) USER_DEFS+=("$1"); shift;;
--bank) BANK_SPECS+=("$2"); shift 2;; --bank) BANK_SPECS+=("$2"); shift 2;;
--bank-data) BANK_LOCAL_DATA=1; shift;;
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;; --mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
--max-allocs) MAX_ALLOCS="$2"; shift 2;; --max-allocs) MAX_ALLOCS="$2"; shift 2;;
--gfx) GFX_MODE="$2"; shift 2;; --gfx) GFX_MODE="$2"; shift 2;;
@@ -366,7 +374,7 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
# 2. user sources → .rel (HOME) # 2. user sources → .rel (HOME)
USER_RELS=() USER_RELS=()
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}") CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}" "${USER_DEFS[@]}")
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT) [[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS") [[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
for src in "${SOURCES[@]}"; do for src in "${SOURCES[@]}"; do
@@ -420,8 +428,19 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then
bank_n="${spec%%=*}" bank_n="${spec%%=*}"
bank_src="${spec#*=}" bank_src="${spec#*=}"
rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel" rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel"
# По умолчанию --dataseg НЕ задаём: писучие данные банкового
# модуля (глобалы, file-static, статики функций) остаются в
# общем _DATA, то есть в W1/W2 — они замаплены всегда и видны
# и банку, и остальному коду. С --dataseg BANKn они уезжают в
# СТРАНИЦУ БАНКА и снаружи не читаются (проверено на .map:
# глобал банкового модуля лёг по 0x0001C000): это экономит
# W1/W2, но требует ручного контроля видимости, поэтому
# включается явно через --bank-data.
bank_data_flags=()
[[ $BANK_LOCAL_DATA -eq 1 ]] && bank_data_flags=(--dataseg "BANK${bank_n}")
run "$SDCC" "${CC_FLAGS[@]}" \ run "$SDCC" "${CC_FLAGS[@]}" \
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \ --codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" \
"${bank_data_flags[@]}" \
-c -o "$rel" "$bank_src" -c -o "$rel" "$bank_src"
BANK_RELS+=("$rel") BANK_RELS+=("$rel")
# Virtual address: bank_n in upper byte, BANK_LOW16 in low half. # Virtual address: bank_n in upper byte, BANK_LOW16 in low half.
+17 -10
View File
@@ -1,5 +1,5 @@
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
accfill 3772 accfill 3786
argv 3431 argv 3431
assrtest 3847 assrtest 3847
atlas 9193 atlas 9193
@@ -9,15 +9,15 @@ banked 1056
bankedbg 1067 bankedbg 1067
banklocl 4683 banklocl 4683
banktest 3767 banktest 3767
bgi_img 7057 bgi_img 7200
bgitest 3737 bgitest 3751
bios_text 4461 bios_text 4461
blitperf 5860 blitperf 5860
blitw 4939 blitw 4939
cat 913 cat 913
cblstream 6217 cblstream 6431
cbltest 6366 cbltest 6580
cblwav 6434 cblwav 6648
conio 4605 conio 4605
conio2 3929 conio2 3929
dec_test 860 dec_test 860
@@ -27,12 +27,12 @@ fdmax 6023
filetest 10547 filetest 10547
fpsdiv 4870 fpsdiv 4870
gets 509 gets 509
gfx_dbuf 5032 gfx_dbuf 5112
gfx_demo 4103 gfx_demo 4117
gfxbanks 6044 gfxbanks 6044
hello 4167 hello 4167
hello2 4244 hello2 4244
irqtest 6527 irqtest 6647
kbdraw 4911 kbdraw 4911
ls 4835 ls 4835
malloc 4461 malloc 4461
@@ -45,7 +45,8 @@ palfile 5406
ptime 5744 ptime 5744
rpgprof 12082 rpgprof 12082
rpgwalk 10309 rpgwalk 10309
rt_test 4892 rt_test 5154
scroll 2974
seek 4188 seek 4188
simple 955 simple 955
solidt 11531 solidt 11531
@@ -60,4 +61,10 @@ strtest 1340
text_palette 5024 text_palette 5024
timedir 5454 timedir 5454
w0page 8831 w0page 8831
w3bankgfx 5240
w3bgfx 5246
w3big 3673
w3huge 3700
w3probe 3556
w3tiny 3471
winrest 4459 winrest 4459
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
2 accfill
3 argv
4 assrtest
5 atlas
9 bankedbg
10 banklocl
11 banktest
12 bgi_img
13 bgitest
14 bios_text
15 blitperf
16 blitw
17 cat
18 cblstream
19 cbltest
20 cblwav
21 conio
22 conio2
23 dec_test
27 filetest
28 fpsdiv
29 gets
30 gfx_dbuf
31 gfx_demo
32 gfxbanks
33 hello
34 hello2
35 irqtest
36 kbdraw
37 ls
38 malloc
45 ptime
46 rpgprof
47 rpgwalk
48 rt_test
49 scroll
50 seek
51 simple
52 solidt
61 text_palette
62 timedir
63 w0page
64 w3bankgfx
65 w3bgfx
66 w3big
67 w3huge
68 w3probe
69 w3tiny
70 winrest
+49
View File
@@ -0,0 +1,49 @@
/*
* gfx_blit_cols_part_noclip — блит спрайта column-major БЕЗ клипа по экрану,
* с обрезкой сверху (как gfx_blit_cols_part). Пара к gfx_blit_cols_part
* ровно так же, как gfx_blit_part_noclip — к gfx_blit_part.
*
* Зачем: у персонажей PoP спрайт почти всегда целиком на экране, а общий
* gfx_blit_cols_part платит за проверку четырёх границ, пересчёт w/h/cx/sy и
* 16-битную арифметику независимо от того, вылезает край или нет — замер в
* MAME (roomtest, страж + клинок): 5 622 такта подготовки против 13 596 на
* сам accel-проход. Здесь та же колонка уходит в _bgi_blit_cols_raw без
* подготовки.
*
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
* - прямоугольник назначения целиком на экране;
* - ширина и высота кадра <= 255, y <= 255;
* - skip < высоты кадра.
* Не выполняется — зовите gfx_blit_cols_part.
*
* Флип, как и в gfx_blit_cols_part, бесплатный: подаём src с последней
* колонки и отрицательный страйд — accel сам кладёт её в левый x.
*
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
* собранного с --w3, её вызывать нельзя — после _bgi_begin окно W3 занято
* видеобанком и код вызывающего исчезает из адресного пространства.
*/
#include <stdint.h>
#include "../_bgi.h"
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows)
{
const uint8_t *p = (const uint8_t *)img;
uint8_t w = p[0]; /* ширина кадра (< 256) */
uint8_t fh = p[2]; /* высота кадра (< 256) */
const uint8_t *src = p + 4;
uint8_t h;
if (w == 0 || fh == 0 || skip >= fh) return;
h = (uint8_t)(fh - skip);
if (rows && rows < h) h = rows;
if (flip) src += (uint16_t)(w - 1) * fh; /* последняя колонка -> левый x */
src += skip;
_bgi_begin();
_bgi_blit_cols_raw(src, (uint8_t *)(_gfx_addr_base + (uint16_t)x),
w, h, flip ? -(int)fh : (int)fh, (uint8_t)y);
_bgi_end();
}
+38
View File
@@ -0,0 +1,38 @@
/*
* gfx_blit_part_noclip — блит ПОД-ПРЯМОУГОЛЬНИКА картинки getimage-формата
* БЕЗ клипа по экрану, в ТЕКУЩЕМ банке (gfx_set_bank). Пара к
* gfx_blit_noclip ровно так же, как gfx_blit_part — к gfx_blit.
*
* Зачем: тайловому движку часто нужно положить поверх спрайта не весь
* тайл, а только ту его часть, что перекрывает спрайт. Через
* gfx_blit_part это идёт общим ядром _gfx_blit_full (16-битная
* арифметика, клип, нарезка полос) — ~13 К тактов накладных на вызов
* независимо от размера; здесь тот же под-прямоугольник рисуется линейным
* спрайтовым ядром (~4.6 К на 32×3). Замер PoP roomtest 2026-07-29:
* fore-слой поверх персонажа рисовал 9 целых тайлов за 432 К тактов
* (стена = блок 64×63); с обрезкой по прямоугольнику спрайта тех же
* пикселей остаётся втрое меньше.
*
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
* - под-прямоугольник (sx,sy,w,h) лежит внутри картинки;
* - прямоугольник назначения целиком на экране;
* - w,h <= 255, y <= 255.
* Не выполняется — зовите gfx_blit_part.
*
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
* собранного с --w3, её вызывать нельзя — после _bgi_begin окно W3 занято
* видеобанком и код вызывающего исчезает из адресного пространства.
*/
#include "../_bgi.h"
void gfx_blit_part_noclip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h)
{
const uint8_t *p = (const uint8_t *)img;
uint16_t stride = (uint16_t)(p[0] | (p[1] << 8));
const uint8_t *src = p + 4 + (uint16_t)sy * stride + sx;
_bgi_begin();
_gfx_blit_sprite_noclip(x, y, src, stride, w, h);
_bgi_end();
}
+18
View File
@@ -105,6 +105,14 @@ void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h);
void gfx_blit_part(int x, int y, const void *img, void gfx_blit_part(int x, int y, const void *img,
int sx, int sy, int w, int h); int sx, int sy, int w, int h);
/* Тот же под-прямоугольник БЕЗ клипа по экрану — линейным спрайтовым
* ядром (пара к gfx_blit_noclip). Вызывающий гарантирует: под-
* прямоугольник внутри img, назначение целиком на экране, w,h,y <= 255.
* Для тайловых движков, кладущих поверх спрайта только перекрывающую его
* часть тайла (PoP: fore-слой). */
void gfx_blit_part_noclip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h);
/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel- /* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel-
* проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без * проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без
* CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot- * CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot-
@@ -120,6 +128,16 @@ void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows); int skip, int rows);
/* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как
* gfx_blit_part_noclip к gfx_blit_part. Вызывающий гарантирует: спрайт
* целиком на экране, ширина/высота кадра и y <= 255, skip < высоты.
* rows = 0 — «до низа кадра». Экономит ~5.6 К тактов подготовки на вызов
* (замер PoP roomtest: подготовка 5 622 против 13 596 на сам accel-проход),
* что для персонажа со спрайтом-компаньоном (клинок) даёт заметную долю
* кадра. */
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows);
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/ /* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
* оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда * оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда
* работает банком GFX_BANK_NORMAL независимо от gfx_set_bank; * работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
+4 -4
View File
@@ -30,10 +30,10 @@
#define _IRQ_VEC_BUF_SIZE (256 + 257) #define _IRQ_VEC_BUF_SIZE (256 + 257)
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в /* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
* W1). С запасом над реальным размером трамплина (244 Б после * W1). С запасом над реальным размером трамплина (267 Б после
* FIFO-drain + overrun-recovery в raw-клавиатурной ветке, 2026-07-18 — * упаковки held-карты клавиатуры в биты, 2026-07-30 — проверять
* проверять __irq_tramp_end при правках, запас держать 60 Б). */ * __irq_tramp_end при правках, запас держать >= 60 Б). */
#define _IRQ_TRAMP_BUF_SIZE 320 #define _IRQ_TRAMP_BUF_SIZE 336
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
View File
+31 -5
View File
@@ -10,7 +10,8 @@
* (порт данных 0x18 читается один раз, деструктивно — либо мы, * (порт данных 0x18 читается один раз, деструктивно — либо мы,
* либо DSS, не оба), декодируется make/break (PS/2 Scan Code * либо DSS, не оба), декодируется make/break (PS/2 Scan Code
* Set 2: 0xF0 — префикс отпускания, 0xE0 — префикс расширенной * Set 2: 0xF0 — префикс отпускания, 0xE0 — префикс расширенной
* клавиши) в битовую карту `_kbdraw_down[]`, и к DSS НЕ идём * клавиши) в битовую карту `_kbdraw_down[]` (бит на код, 64 Б),
* и к DSS НЕ идём
* (private RETI — тот же паттерн, что и у CBL ниже). Пока raw * (private RETI — тот же паттерн, что и у CBL ниже). Пока raw
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>). * открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>).
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке * - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
@@ -61,6 +62,7 @@ void _irq_tramp(void) __naked
;; break-кода = клавиша «залипает» навсегда (найдено 2026-07-16). ;; break-кода = клавиша «залипает» навсегда (найдено 2026-07-16).
push hl push hl
push de push de
push bc ; маска бита строится в BC (см. ниже)
tr_kbd_drain: tr_kbd_drain:
in a, (#_IRQ_SIO_DATA) ; читаем тем самым дренируем байт in a, (#_IRQ_SIO_DATA) ; читаем тем самым дренируем байт
cp #0xF0 cp #0xF0
@@ -68,7 +70,27 @@ void _irq_tramp(void) __naked
cp #0xE0 cp #0xE0
jr Z, tr_kbd_setext jr Z, tr_kbd_setext
;; --- обычный байт кода: индекс в _kbdraw_down[код(+256 EXT)] --- ;; --- обычный байт кода: БИТ code&7 в байте _kbdraw_down[code>>3]
;; (карта упакована по битам: 64 Б вместо 512, см. _kbdraw.h).
;; Маску строим сдвигом, а НЕ таблицей: таблица потребовала бы
;; `ld hl,#метка` внутри трамплина, а он копируется в W2 побайтно
;; и обязан быть без абсолютных само-ссылок (см. шапку). ---
ld e, a ; E = код
and #7
ld b, a
inc b ; B = (код&7)+1 для djnz
ld a, #1
jr tr_kbd_mskdec
tr_kbd_mskloop:
add a, a
tr_kbd_mskdec:
djnz tr_kbd_mskloop ; A = 1 << (код&7)
ld c, a ; C = маска бита
ld a, e
rrca
rrca
rrca
and #0x1F ; A = код>>3 (код <= 255)
ld hl, #__kbdraw_down ld hl, #__kbdraw_down
ld d, #0 ld d, #0
ld e, a ld e, a
@@ -76,16 +98,19 @@ void _irq_tramp(void) __naked
ld a, (__kbdraw_pending) ld a, (__kbdraw_pending)
bit 1, a ; _KBDRAW_PEND_EXT? bit 1, a ; _KBDRAW_PEND_EXT?
jr Z, tr_kbd_noext jr Z, tr_kbd_noext
ld de, #256 ld de, #32 ; +256 кодов = +32 байта карты
add hl, de add hl, de
tr_kbd_noext: tr_kbd_noext:
ld a, (__kbdraw_pending) ld a, (__kbdraw_pending)
bit 0, a ; _KBDRAW_PEND_BREAK? bit 0, a ; _KBDRAW_PEND_BREAK?
jr Z, tr_kbd_mkdown jr Z, tr_kbd_mkdown
xor a, a ; break снять (0) ld a, c
cpl ; break сбросить бит
and a, (hl)
jr tr_kbd_wr jr tr_kbd_wr
tr_kbd_mkdown: tr_kbd_mkdown:
ld a, #1 ; make взвести (1) ld a, c ; make взвести бит
or a, (hl)
tr_kbd_wr: tr_kbd_wr:
ld (hl), a ld (hl), a
xor a, a xor a, a
@@ -120,6 +145,7 @@ void _irq_tramp(void) __naked
ld a, #1 ld a, #1
ld (__kbdraw_overrun), a ld (__kbdraw_overrun), a
tr_kbd_noovr: tr_kbd_noovr:
pop bc
pop de pop de
pop hl pop hl
pop af pop af
+7 -2
View File
@@ -23,7 +23,12 @@ extern uint8_t _kbdraw_pending; /* FSM декодера make/break, см. в
extern uint8_t _kbdraw_overrun; /* 1 = ISR поймал Rx-overrun SIO (потерян extern uint8_t _kbdraw_overrun; /* 1 = ISR поймал Rx-overrun SIO (потерян
* байт → возможно потерян break); снимает * байт → возможно потерян break); снимает
* kbd_raw_sync (чистит _kbdraw_down) */ * kbd_raw_sync (чистит _kbdraw_down) */
extern uint8_t _kbdraw_down[512]; /* 0/1 по коду; +256 для расширенных /* Held-состояние: БИТ на скан-код, код>>3 = байт, код&7 = номер бита;
* (префикс 0xE0) — см. kbd_raw_down */ * коды 0..255 обычные, 256..511 — расширенные (префикс 0xE0), т.е.
* смещение +32 байта. Упаковка вместо байта на код: 64 Б вместо 512 —
* заметная доля _DATA на 32-килобайтной раскладке (замер PoP roomtest
* 2026-07-30). Ведёт трамплин прерывания (libc/irq/_irq_tramp.c),
* читает kbd_raw_down(). */
extern uint8_t _kbdraw_down[64];
#endif #endif
+1 -1
View File
@@ -9,4 +9,4 @@ uint8_t _kbdraw_active;
uint8_t _kbdraw_pending; uint8_t _kbdraw_pending;
uint8_t _kbdraw_overrun; /* ISR взвёл при Rx-overrun SIO (потерян uint8_t _kbdraw_overrun; /* ISR взвёл при Rx-overrun SIO (потерян
* break) — kbd_raw_sync снимет залипшее */ * break) — kbd_raw_sync снимет залипшее */
uint8_t _kbdraw_down[512]; uint8_t _kbdraw_down[64]; /* БИТ на скан-код (см. _kbdraw.h) */
+1 -1
View File
@@ -10,5 +10,5 @@
uint8_t kbd_raw_down(uint16_t code) uint8_t kbd_raw_down(uint16_t code)
{ {
if (code >= 512) return 0; if (code >= 512) return 0;
return _kbdraw_down[code]; return (uint8_t)((_kbdraw_down[code >> 3] >> (code & 7)) & 1);
} }
+10 -2
View File
@@ -36,7 +36,15 @@ void kbd_raw_sync(void)
if (!_kbdraw_overrun) return; if (!_kbdraw_overrun) return;
_kbdraw_overrun = 0; _kbdraw_overrun = 0;
for (i = 0; i < 6; i++) save[i] = _kbdraw_down[_kbd_mod_codes[i]]; /* Карта упакована по битам (см. _kbdraw.h): сохраняем/возвращаем
* именно БИТ модификатора, а не байт. */
for (i = 0; i < 6; i++) {
uint16_t c = _kbd_mod_codes[i];
save[i] = (uint8_t)((_kbdraw_down[c >> 3] >> (c & 7)) & 1);
}
memset(_kbdraw_down, 0, sizeof(_kbdraw_down)); memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
for (i = 0; i < 6; i++) _kbdraw_down[_kbd_mod_codes[i]] = save[i]; for (i = 0; i < 6; i++) {
uint16_t c = _kbd_mod_codes[i];
if (save[i]) _kbdraw_down[c >> 3] |= (uint8_t)(1 << (c & 7));
}
} }
+41 -5
View File
@@ -11,6 +11,9 @@
# #
# Использование: # Использование:
# toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ... # toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ...
# toolchain/make_hdd.sh out.chd prog.exe KID:kid0.atl GUARD:g0.atl ...
# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог образа
# (каталог создаётся); без префикса файл идёт в корень.
set -e set -e
if [ $# -lt 2 ]; then if [ $# -lt 2 ]; then
@@ -35,18 +38,51 @@ dd if=/dev/zero of="$RAW" bs=512 count=$SECTORS status=none
printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC" printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC"
export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1 export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16. # mtools собран с enable-raw-term и, когда ему нужно что-то спросить,
mpartition -I -c -t $CYLS -h $HEADS -s $SECS z: # открывает УПРАВЛЯЮЩИЙ ТЕРМИНАЛ напрямую (/dev/tty) — в обход stdin.
mformat z: # Поэтому ни `< /dev/null`, ни перенаправление stdio не спасают: из
# терминала сборка молча вставала навсегда на `mmd z:/BG` (подтверждено
# lsof: fd 0 = /dev/null, fd 4 = /dev/tty, процесс спит). Лечится только
# отсутствием управляющего терминала — запускаем mtools в новой сессии,
# тогда открыть /dev/tty нечего и mtools идёт неинтерактивным путём.
#
# setsid(1) в macOS нет, поэтому делаем то же самое питоном.
# Каналов, откуда mtools может ждать ответ, ДВА, и закрывать надо оба:
# /dev/tty — убирается новой сессией (os.setsid);
# stdin — убирается перенаправлением из /dev/null.
# Закрыть только один недостаточно: с одним лишь setsid mmd переключался на
# stdin и вис уже на нём (lsof: fd 0 = /dev/ttys002).
mt() {
python3 -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \
"$@" < /dev/null
}
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16.
echo "--- разметка и формат образа ---"
mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
mt mformat z:
echo "--- копирование файлов ---"
# Аргумент может быть либо файлом (кладём в корень), либо парой
# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда.
# Имена на Sprinter/DSS — 8.3, каталоги тоже (KID, GUARD, BG, LEVELS...).
for f in "$@"; do for f in "$@"; do
mcopy -o "$f" z: case "$f" in
*:*) dir="${f%%:*}"; src="${f#*:}"
# Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе
# непрошедший вопрос mtools снова окажется невидимым.
mt mmd "z:/$dir" >/dev/null 2>&1 || true
mt mcopy -o "$src" "z:/$dir/" ;;
*) mt mcopy -o "$f" z: ;;
esac
done done
echo "--- содержимое образа ---" echo "--- содержимое образа ---"
mdir z: mt mdir -/ z:
# RAW -> CHD (без сжатия = быстро; та же геометрия). # RAW -> CHD (без сжатия = быстро; та же геометрия).
echo "--- конвертация RAW -> CHD ---"
chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \ chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \
-chs $CYLS,$HEADS,$SECS -ss 512 -f -chs $CYLS,$HEADS,$SECS -ss 512 -f
echo "готово: $OUT_CHD" echo "готово: $OUT_CHD"