f159aa47e1
Две разные болячки, обе разобраны записью звука MAME в WAV. libc: буфер CBL железо не чистит, а запись в порт управления сразу пускает воспроизведение с нулевого слота — первые 256 сэмплов (23,4 мс) уходит то, что лежало раньше. Своими данными звук идёт лишь с третьей половины: прерывание приходит на 128-м слоте и ставит указатель на противоположную половину. _cbl_prime заливает буфер тишиной сразу после включения (раньше нельзя — запись проходит только при поднятом bit7). В MAME это немо (эмулируемый буфер стартует нулями при двухдополнительном ЦАП), на железе это ровно тот мусор, что ловился на тестовых примерах CBL. PoP: на выходе по ESC звучало ровно 11 мс шума на полной громкости — один пропущенный блок (128 сэмплов). Причина: pop_shutdown освобождал атласы и графику через ESTEX при открытом звуке, насос не успевал долить. Звук гасим первым действием. Проверено записью — всплеска больше нет. Заодно записан разбор стартового всплеска (sound_plan.md §10): это не мусор, а gate_closing_fast из левой комнаты, обрываемый soft_land. Обрыв одноголосьем — поведение оригинала (play_digi_sound начинается с stop_digi, seg009.c:2402). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
982 lines
65 KiB
C
982 lines
65 KiB
C
/*
|
||
* roomtest_cold.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК
|
||
* (--bank 8=roomtest_cold.c, окно W3). Здесь всё, что отрабатывает раз на
|
||
* уровень или только по отладочной клавише; кадровый путь остался в
|
||
* roomtest.c. Зачем разделили и что осталось снаружи — см. roomtest_cold.h.
|
||
*
|
||
* Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого
|
||
* модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие
|
||
* __banked-модули через трамплины.
|
||
*/
|
||
#include <graphics.h>
|
||
#include <gfx.h>
|
||
#include "roomtest_cold.h"
|
||
#include "pop_bg.h" /* pop_loose_mob_reset — банк 7, зовём через трамплин */
|
||
#include "pop_kid.h"
|
||
#include "pop_map.h"
|
||
#include "pop_level.h"
|
||
#include "pop_trob.h"
|
||
#include "pop_guard.h"
|
||
#include "pop_state.h"
|
||
#include "pop_geom.h"
|
||
#include "pop_cdraw.h" /* pop_cd — инвалидация слотов после переворота */
|
||
#include <sprite.h> /* gfx_copy_page */
|
||
#include "pop_tune.h"
|
||
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
||
#include "pop_shadow.h" /* запечённый набор Тени */
|
||
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
||
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
|
||
#include <stdio.h> /* puts/printf — сообщения об ошибках загрузки */
|
||
#include <kbd_raw.h>
|
||
|
||
#define SEQ_STAND 2
|
||
#define SEQ_TURN 5
|
||
#define SEQ_FALL 7
|
||
#define SEQ_RUN 84
|
||
#define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */
|
||
#define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */
|
||
#define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */
|
||
#define TILE_EMPTY 0
|
||
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
||
|
||
/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ —
|
||
* это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью
|
||
* (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72).
|
||
* Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки:
|
||
* его стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина
|
||
* той самой двери. */
|
||
static void find_start_level_door(uint8_t room)
|
||
{
|
||
uint8_t *m = pop_trob_modif(room);
|
||
uint8_t tp;
|
||
if (!m) return;
|
||
/* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз
|
||
* за уровень, а pop_add_trob сам лезет в W0. */
|
||
for (tp = 0; tp < 30; tp++) {
|
||
if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue;
|
||
m[tp] = 43; /* створка поднята полностью */
|
||
pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */
|
||
}
|
||
}
|
||
|
||
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
|
||
* (seg003:028A).
|
||
*
|
||
* Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define:
|
||
* start_pos — тайл 0..29 (row*10+col), start_dir — направление, в котором
|
||
* персонаж должен ОКАЗАТЬСЯ.
|
||
*
|
||
* Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж
|
||
* ставится спиной и доворачивается кадрами позы входа. Сама поза — из
|
||
* tbl_entry_pose (data.h:848 = {0,1,0,…,2,…}):
|
||
* 1 (уровень 1) — падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2);
|
||
* это оно захлопывает решётку за спиной в первой комнате;
|
||
* 2 (уровень 13) — вбегание (seq_84_run);
|
||
* иначе — разворот на месте (seq_5_turn).
|
||
* Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */
|
||
void pop_start_level(void) __banked
|
||
{
|
||
uint8_t entry_pose, start_room, pos, seq = SEQ_TURN, chkp;
|
||
int8_t dir;
|
||
|
||
pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */
|
||
pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */
|
||
/* upside_down тоже сбрасывается стартом уровня (seg003:38/188). Если
|
||
* картинка была перевёрнута, отрисовка комнаты ниже уже пойдёт обычной
|
||
* стороной, поэтому переворачивать ничего не надо — только снять флаги. */
|
||
pop_upside = 0;
|
||
pop_upside_want = 0;
|
||
pop_upside_dirty = 0;
|
||
entry_pose = pop_level_entry_pose(pop_current_level);
|
||
start_room = pop_level_start_room();
|
||
pos = pop_level_start_pos();
|
||
dir = (int8_t)~pop_level_start_dir();
|
||
/* Особое событие: старт с чекпойнта (do_startpos, seg003:0141).
|
||
* Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles()
|
||
* ниже, иначе рестарт её вернёт. */
|
||
chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint);
|
||
if (chkp) {
|
||
start_room = POP_CHKP_RESPAWN_ROOM;
|
||
pos = POP_CHKP_RESPAWN_TILEPOS;
|
||
dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную
|
||
* и разворачивается seq_5 */
|
||
}
|
||
|
||
#ifdef DBG_START_ROOM
|
||
/* ОТЛАДОЧНЫЙ СТАРТ: сразу в целевую комнату оптимизации, минуя проход
|
||
* уровня. Задаётся сборкой (make ROOM=15 POS=2), по умолчанию — сцена
|
||
* 11/15 из docs/perf_l11_room15.md: Кид в (0,2), справа чомпер под
|
||
* факелом, дальше второй факел и страж.
|
||
*
|
||
* Ставится ПОСЛЕ чекпойнта намеренно: отладочная позиция должна
|
||
* перебивать и его тоже, иначе рестарт уровня уводил бы Кида из
|
||
* измеряемой комнаты. Направление и позу входа не трогаем — они из
|
||
* данных уровня, как у обычного старта. */
|
||
/* ...НО ТОЛЬКО НА СВОЁМ УРОВНЕ. pop_start_level зовётся ещё и на
|
||
* СМЕНЕ уровня, и без этой проверки отладочная позиция перехватывала
|
||
* переход: на 6-м Кид проваливается в комнате 1, а на 7-м обязан
|
||
* появиться в комнате 17 и продолжить падение вниз (спецсобытие
|
||
* «вход падением», см. ниже). Вместо этого он оказывался в
|
||
* DBG_START_ROOM/POS, спецсобытие не срабатывало вовсе, и переход 6->7
|
||
* выглядел сломанным (поймано пользователем 2026-08-20 на сборке
|
||
* make LEVEL=6 ROOM=1 POS=18).
|
||
*
|
||
* Рестарт ТОГО ЖЕ уровня (смерть, чекпойнт) отладочную позицию
|
||
* сохраняет — ради этого она и перебивает чекпойнт выше. */
|
||
if (pop_current_level == FIRST_LEVEL) {
|
||
start_room = DBG_START_ROOM;
|
||
pos = DBG_START_POS;
|
||
}
|
||
#endif
|
||
|
||
/* Рестарт уровня = load_level() заново (play_level, seg003:57): в
|
||
* исходное возвращаются И модификаторы (пики/ворота), И сами тайлы —
|
||
* разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и
|
||
* убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */
|
||
pop_trob_reset();
|
||
pop_level_reset_tiles();
|
||
pop_level_reset_guards();
|
||
pop_guard_reset(); /* start_level: Guard.direction = none */
|
||
/* united_with_shadow = 0 (seg003:95) — состояние ПЕРСОНАЖА уровня, а не
|
||
* комнаты: pop_guard_reset зовётся и при каждом входе в комнату (из
|
||
* pop_guard_enter), поэтому гасить слияние надо здесь. */
|
||
pop_united_shadow = 0;
|
||
pop_shadow_init = 0;
|
||
pop_guard_notice_timer = 0; /* фора Джафара — не переживает рестарт */
|
||
pop_nav_hold = 0; /* старт уровня снимает и держатель чита */
|
||
pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */
|
||
pop_loose_forget(); /* ...и фазы плит — уровень восстановлен целиком */
|
||
/* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в
|
||
* комнату-чекпойнт после респавна остался бы непройденным. */
|
||
if (chkp)
|
||
pop_level_set_tile(POP_CHKP_CLEAR_ROOM,
|
||
(uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL),
|
||
TILE_EMPTY);
|
||
|
||
/* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos
|
||
* отрабатывает раньше первого draw_room), иначе шов кадр показывал бы
|
||
* ещё не тронутое состояние решётки. */
|
||
if (entry_pose == ENTRY_POSE_FALL) {
|
||
/* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5,
|
||
* col2, row0 её же типом и модификатором (в оригинале и то и другое
|
||
* берётся из curr_* после get_tile). */
|
||
uint8_t tp = 2; /* row0*10 + col2 */
|
||
pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]);
|
||
seq = SEQ_FALL;
|
||
} else if (entry_pose == ENTRY_POSE_RUN) {
|
||
seq = SEQ_RUN;
|
||
}
|
||
|
||
/* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть
|
||
* открыта уже в первом кадре (в оригинале find_start_level_door стоит
|
||
* между do_startpos и draw_level_first). */
|
||
find_start_level_door(start_room);
|
||
|
||
enter_room(start_room);
|
||
kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir);
|
||
#ifdef DBG_START_ROOM
|
||
/* ...и поправить x: kid_init ставит `x_bump[col] + TILE_SIZEX`, а это
|
||
* ЛЕВАЯ ГРАНИЦА СЛЕДУЮЩЕЙ колонки (порт set_start_pos — в данных уровня
|
||
* стартовые позиции подобраны под такую договорённость). Отладочный
|
||
* старт называет тайл, В КОТОРОМ Кид должен оказаться, поэтому сдвигаем
|
||
* его внутрь названного: с границы физика относит Кида к колонке правее,
|
||
* и в 11/15 он стартовал прямо в челюстях (найдено пользователем
|
||
* 2026-08-19).
|
||
*
|
||
* Отступ ровно 2, а не «половина тайла»: колонку определяет не сам x, а
|
||
* ВЕСОВАЯ ТОЧКА кадра (determine_col -> dx_weight: dx позы минус
|
||
* weight-биты, с учётом направления), поэтому геометрически ровной
|
||
* середины тут нет. Число снято замером живой сцены: x = 98 при позе
|
||
* стойки даёт curr_col = 2, x = 94 — уже 1. */
|
||
if (pop_current_level == FIRST_LEVEL) { /* см. условие выше */
|
||
Kid.x = (uint8_t)(pop_x_bump[(pos % 10) + FIRST_ONSCREEN_COLUMN] +
|
||
TILE_SIZEX - 2);
|
||
Kid.curr_col = (int8_t)(pos % 10);
|
||
}
|
||
#endif
|
||
/* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м
|
||
* уровне Кид ставится в комнату 17, а экран тут же переводится на
|
||
* комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) —
|
||
* он влетает в неё сверху, продолжая падение с шестого уровня. */
|
||
if (pop_current_level == FALLING_ENTRY_LEVEL &&
|
||
start_room == FALLING_ENTRY_ROOM) {
|
||
uint8_t below = pop_room_link(start_room, LINK_DOWN);
|
||
if (below) {
|
||
Kid.y = (uint8_t)(Kid.y - 189);
|
||
Kid.curr_row = pop_y_to_row((int16_t)Kid.y);
|
||
enter_room(below);
|
||
}
|
||
}
|
||
Kid.sword = 0; /* set_start_pos: sword_0_sheathed */
|
||
/* play_level (seg003:106): have_sword = level >= have_sword_from_level
|
||
* (=2) — начиная со второго уровня меч у Кида уже есть. */
|
||
pop_have_sword = (uint8_t)(pop_current_level >= 2);
|
||
/* seamless (seg003:158): пришли БЕЗ двери уровня, прямо на бегу — HP не
|
||
* трогаем, оно переезжает как есть. Флаг одноразовый: у оригинала его
|
||
* гасит заставка «LEVEL N», которую этот же переход и пропускает. */
|
||
if (pop_seamless) { pop_seamless = 0; pop_kid_hp_carry(); }
|
||
else pop_kid_hp_reset();
|
||
}
|
||
|
||
/* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- *
|
||
*
|
||
* Нужен, чтобы пройти все комнаты уровня и собрать список багов отрисовки,
|
||
* включая недостижимые обычным путём.
|
||
*
|
||
* Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната
|
||
* смотрелась в исходном состоянии». Убран: reset обнуляет room_seen, и
|
||
* модификаторы перечитываются из уровня заново, то есть чит ОТМАТЫВАЕТ МИР
|
||
* — открытые/закрытые ворота, выдвинутые пики, нажатые кнопки. Навигация
|
||
* обязана только телепортировать; исходное состояние даёт перезапуск. */
|
||
uint8_t pop_dbg_roomnav(uint8_t dir) __banked
|
||
{
|
||
uint8_t r, i, c, pos = 20, any = 0xFF;
|
||
|
||
/* Кид МЁРТВ: сначала честный рестарт уровня, потом телепорт. Навигация
|
||
* только переставляет Кида (kid_init + сброс HP), то есть «оживляет» его
|
||
* мимо start_level — а тот обязан вернуть тайлы, модификаторы и ОЧИСТИТЬ
|
||
* слот соперника (seg003:100: Guard.direction = dir_56_none). Без этого
|
||
* после смерти оставался живой скелет рядом с вернувшейся кучей костей. */
|
||
if (pop_kid_dead) pop_start_level();
|
||
|
||
r = cur_room;
|
||
r = (dir == 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
|
||
: (uint8_t)(r == 1 ? 24 : r - 1);
|
||
enter_room(r);
|
||
|
||
/* Куда поставить Кида в новой комнате: пол С МАКСИМАЛЬНЫМ Y — сначала
|
||
* НИЖНИЙ ряд (2), потом 1, и только потом 0 (пользователь 2026-08-11).
|
||
* Раньше брался первый пол подряд, то есть почти всегда (0,0) — верхний
|
||
* левый угол: это и «прилёт» в проём/ворота, и, на уровне 5, посадка
|
||
* вплотную к тени спецсобытия, которая ждёт в колонке −1 ряда 0. Вместо
|
||
* сцены кражи зелья начиналась схватка, которой в этой комнате быть не
|
||
* должно. Снизу вверх нужно и самой навигации: в верхнем ряду пола чаще
|
||
* нет, и Кид проваливается из комнаты сразу после телепорта.
|
||
*
|
||
* ИСКЛЮЧЕНИЕ — комната 14 уровня 7 (пользователь 2026-08-12): её проходят
|
||
* СВЕРХУ ВНИЗ (с ряда 0 на ряд 2 через зацеп за кромки), и с нижнего ряда
|
||
* этот спуск не проверить вовсе. Пол в ряду 0 там есть, так что Кид не
|
||
* проваливается.
|
||
*
|
||
* В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его там нет —
|
||
* любой проходимый tile_is_floor (seg001: всё, кроме пустоты, верха
|
||
* колонны, верха двери, стены и «половинок» 26..29). */
|
||
{
|
||
int8_t row, step = -1, first = 2;
|
||
if (pop_current_level == GRAB_DOWN_LEVEL &&
|
||
r == GRAB_DOWN_ROOM) { first = 0; step = 1; }
|
||
for (row = first; row >= 0 && row <= 2 && any == 0xFF;
|
||
row = (int8_t)(row + step)) {
|
||
uint8_t fallback = 0xFF;
|
||
for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) {
|
||
c = (uint8_t)(room_fg[i] & 0x1F);
|
||
if (c == 1) { any = i; break; }
|
||
if (fallback == 0xFF && c && c != 9 && c != 12 &&
|
||
c != 20 && !(c >= 26 && c <= 29)) fallback = i;
|
||
}
|
||
if (any == 0xFF) any = fallback;
|
||
}
|
||
}
|
||
if (any != 0xFF) pos = any;
|
||
kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0);
|
||
pop_kid_hp_reset();
|
||
|
||
/* ВЫХОД ИЗ БОЯ (BUG-CHEAT-FIGHT-1). Телепорт с вынутым мечом заклинивал
|
||
* управление намертво: pop_control при Kid.sword == SWORD_2_DRAWN уходит
|
||
* в control_with_sword, а там единственный выход — ветка «убрать меч» на
|
||
* кадре FRAME_171_STAND_WITH_SWORD. После kid_init(SEQ_STAND) кадр
|
||
* обычный, стража в новой комнате нет, swordfight не запускается — и
|
||
* диспетчер каждый кадр не делает НИЧЕГО. Это болезнь ЧИТА: в оригинале
|
||
* телепорта между комнатами нет, попасть в режим боя без соперника
|
||
* нечем. Меч в инвентаре (pop_have_sword) НЕ теряем — только состояние
|
||
* схватки. */
|
||
Kid.sword = SWORD_0_SHEATHED;
|
||
holding_sword = 0;
|
||
offguard = 0;
|
||
guard_refrac = 0;
|
||
|
||
/* ПРОВАЛ НА СЛЕДУЮЩИЙ УРОВЕНЬ ПО ЧИТУ (та же болезнь, что выше).
|
||
* Уровень 12 кончается ФАКТОМ попадания в комнату 23 — двери у него нет
|
||
* (pop_seamless, seg000:0900). А навигация перебирает комнаты ПО
|
||
* НОМЕРУ, то есть любой обход рано или поздно наступает на 23-ю, и
|
||
* уровень молча меняется на 13-й: комнату 23 двенадцатого уровня читом
|
||
* не посмотреть в принципе, а прогон «съезжает» на соседний уровень
|
||
* незаметно для проверяющего (поймано 2026-08-13 на первом же прогоне).
|
||
* Держим триггер, пока Кид не сменит комнату НОГАМИ. */
|
||
pop_nav_hold = 1;
|
||
return 1;
|
||
}
|
||
|
||
|
||
/* ---- ЧИТЫ (seg000: switch под if (cheats_enabled)) -------------------- *
|
||
* Переехали из главного цикла в банк 8: дёргаются раз в кадр, то есть
|
||
* один трамплин на кадр, а места в резиденте W1/W2 не осталось (звук,
|
||
* атлас Тени и пейсинг съели остаток; см. TASKS_OPEN, MEM).
|
||
*
|
||
* Флаги «фронта» стали статиками модуля — раньше они были локалями main.
|
||
* Смысл прежний: одно нажатие = одно срабатывание.
|
||
*/
|
||
static uint8_t cheat_prev, immo_prev, sound_prev, nlvl_prev;
|
||
static uint8_t upsd_prev, xdec_prev, xinc_prev;
|
||
|
||
void pop_cheat_tick(void) __banked
|
||
{
|
||
/* Читы (seg000: switch под if (cheats_enabled)). Все — по
|
||
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
|
||
* K — убить стража;
|
||
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
|
||
* иначе боёвку проверять получается ровно три раза);
|
||
* Ctrl+S — вкл/выкл звук (порт Ctrl+S из SDLPoP, seg000:657).
|
||
* НЕ чит: работает и без pop_cheats, как в оригинале.
|
||
* Ctrl обязателен — голая S у нас свободна, но менять
|
||
* пользовательскую раскладку оригинала незачем;
|
||
* Shift+L — следующий уровень (seg000:698, ветка next_level =
|
||
* current_level + 1). Единственный вменяемый способ
|
||
* тестировать уровни 2..15. NB: L без Shift — отладочный
|
||
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
|
||
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
|
||
* что конфликта на практике нет;
|
||
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
|
||
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
|
||
* теряет нажатия при быстрой отправке, и подогнать Кида в
|
||
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
|
||
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
|
||
* персонажа в этом порте задаёт не диспетчер (грабли
|
||
* L1-EXIT). Колонку не трогаем — её пересчитает физика
|
||
* следующего кадра. */
|
||
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
|
||
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
|
||
uint8_t s = (uint8_t)(kbd_raw_down(KBD_SOUND_TOGGLE) &&
|
||
(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL)));
|
||
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
|
||
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
|
||
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
|
||
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
|
||
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
|
||
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
|
||
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
|
||
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */
|
||
if (pop_cheats && i && !immo_prev)
|
||
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
|
||
if (s && !sound_prev) pop_sfx_toggle(); /* не под pop_cheats */
|
||
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
|
||
pop_next_level = (uint8_t)(pop_current_level + 1);
|
||
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
|
||
* Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */
|
||
if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */
|
||
pop_upside_want = (uint8_t)!pop_upside_want;
|
||
pop_upside_dirty = 1;
|
||
}
|
||
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
|
||
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
|
||
cheat_prev = k;
|
||
immo_prev = i;
|
||
sound_prev = s;
|
||
nlvl_prev = nl;
|
||
upsd_prev = up;
|
||
xdec_prev = xd;
|
||
xinc_prev = xi;
|
||
}
|
||
|
||
/* ---- ОТЛАДКА: состояние прогона в верхнем борте ---------------------- *
|
||
*
|
||
* Четыре блока ПАЛОЧКАМИ, каждый своим цветом (outtextxy не годится: текст
|
||
* тянет системный знакогенератор, _gfx_font_buf — 2 КБ статики в W2, и
|
||
* сажает кучу). Двузначные числа — двумя группами: слева десятки, справа
|
||
* единицы, || |||| = 24.
|
||
*
|
||
* жёлтый x 10.. 85 уровень (1..15)
|
||
* белый x 100..175 комната (1..24)
|
||
* голубой x 190..205 скорость (1 палочка NORMAL, 2 FAST, 3 FASTEST)
|
||
* красный x 220..230 бессмертие (1 палочка = только бой, 2 = и мелкий урон)
|
||
* пурпурный x 245 звук ВЫКЛЮЧЕН (Ctrl+S) либо не поднялся вовсе
|
||
|
||
* Звук показан «от противного» — палочка есть, когда звука НЕТ. Иначе
|
||
* индикатор ничего не сообщает: включённый звук слышно и так, а вот
|
||
* молчание неотличимо от «в этот момент нечему звучать».
|
||
*
|
||
* ЦВЕТА — индексы ИГРОВОЙ палитры, и выбраны они не по вкусу, а по двум
|
||
* ограничениям.
|
||
*
|
||
* Первое: записи палитры лежат в ФОРМАТЕ BGR (BIOS $A4), а не RGB — в
|
||
* kid.pal 0x3E это `55 ff ff`, то есть жёлтый, а не голубой. Читать файл
|
||
* «как привычно» нельзя, цвета выйдут переставленными (на этом я и
|
||
* попался в первом заходе).
|
||
*
|
||
* Второе, важнее: часть палитры ПЕРЕЗАПИСЫВАЕТСЯ на ходу — 0x40..0x4F
|
||
* зелья, 0x50..0x5F env, 0x60..0x6F стена (эти два меняются при смене
|
||
* ТАЙЛСЕТА, подземелье/дворец), 0x90..0x9F страж. Прежняя метка комнаты
|
||
* рисовалась цветом 0x57, то есть из env-диапазона: белой она была только
|
||
* в подземелье, а во дворце брала случайный цвет тайлсета. Здесь берём
|
||
* только 0x3A..0x3F — их не трогает никто:
|
||
* 0x3E жёлтый (255,255,85), 0x3F белый, 0x3B голубой (85,255,255),
|
||
* 0x3C красный (226,0,48) — тот же, которым игра даёт вспышку урона.
|
||
*
|
||
* Перерисовка change-driven: пока ничего из показанного не изменилось, в
|
||
* борт не пишем вовсе. Счётчик 2 — обе страницы дабл-буфера. */
|
||
|
||
static uint8_t room_label_sig = 0xFF; /* последняя показанная комната */
|
||
static uint8_t lvl_label_sig = 0xFF; /* ...уровень */
|
||
static uint8_t spd_label_sig = 0xFF; /* ...режим скорости */
|
||
static uint8_t imm_label_sig = 0xFF; /* ...бессмертие */
|
||
static uint8_t snd_label_sig = 0xFF; /* ...звук (pop_snd_want) */
|
||
static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */
|
||
|
||
/* Группа палочек: n штук от x вправо, шаг 5, ширина 2. */
|
||
static void label_bars(int x, uint8_t n)
|
||
{
|
||
uint8_t i;
|
||
for (i = 0; i < n; i++) bar(x + i * 5, 0, x + 1 + i * 5, 5);
|
||
}
|
||
|
||
void pop_dbg_room_label(uint8_t room) __banked
|
||
{
|
||
/* СНАЧАЛА сохранить, потом сравнивать — обход sdcc_z80_cmp_store_a_bug
|
||
* (`if (n != g) g = n;` пишет разность вместо значения). */
|
||
uint8_t prev_room = room_label_sig, prev_lvl = lvl_label_sig;
|
||
uint8_t prev_spd = spd_label_sig, prev_imm = imm_label_sig;
|
||
uint8_t prev_snd = snd_label_sig;
|
||
uint8_t lvl = pop_current_level, spd = pop_speed_mode, imm = pop_immortal;
|
||
uint8_t snd = pop_snd_want;
|
||
room_label_sig = room;
|
||
lvl_label_sig = lvl;
|
||
spd_label_sig = spd;
|
||
imm_label_sig = imm;
|
||
snd_label_sig = snd;
|
||
if (room != prev_room || lvl != prev_lvl || spd != prev_spd ||
|
||
imm != prev_imm || snd != prev_snd)
|
||
room_label_redraw = 2;
|
||
if (!room_label_redraw) return;
|
||
|
||
setfillstyle(SOLID_FILL, BLACK);
|
||
bar(10, 0, 250, 5);
|
||
|
||
setfillstyle(SOLID_FILL, 0x3E); /* уровень — жёлтый */
|
||
label_bars(10, (uint8_t)(lvl / 10));
|
||
label_bars(40, (uint8_t)(lvl % 10));
|
||
|
||
setfillstyle(SOLID_FILL, 0x3F); /* комната — белый */
|
||
label_bars(100, (uint8_t)(room / 10));
|
||
label_bars(130, (uint8_t)(room % 10));
|
||
|
||
/* Скорость: номер режима + 1, то есть NORMAL 1, FAST 2, FASTEST 3 —
|
||
* чем больше палочек, тем быстрее (см. нумерацию в pop_pace.h). */
|
||
setfillstyle(SOLID_FILL, 0x3B); /* скорость — голубой */
|
||
label_bars(190, (uint8_t)(spd + 1));
|
||
|
||
if (imm) { /* бессмертие — красный */
|
||
setfillstyle(SOLID_FILL, 0x3C);
|
||
label_bars(220, imm); /* палочек = уровень (1 или 2) */
|
||
}
|
||
if (!snd) { /* звука НЕТ — пурпурный */
|
||
setfillstyle(SOLID_FILL, 0x3D);
|
||
label_bars(245, 1);
|
||
}
|
||
room_label_redraw--;
|
||
}
|
||
|
||
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
|
||
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
|
||
* активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
|
||
/* side — куда ушёл КИД (0 left, 1 right, 2 up, 3 down); 0xFF = «не переход
|
||
* за Кидом» (старт уровня, респавн, чит-навигация). От него зависит ровно
|
||
* одно: пойдёт ли страж следом (pop_guard_follow, порт exit_room+
|
||
* follow_guard) или останется в прежней комнате. */
|
||
void enter_room_side(uint8_t room, uint8_t side) __banked
|
||
{
|
||
uint8_t pg;
|
||
/* Страж идёт за Кидом? — решаем ДО того, как что-либо загрузим: функции
|
||
* нужны и старая комната (Guard.room), и целевая. Пошёл — ни leave, ни
|
||
* enter звать нельзя: он уже перенесён и живёт дальше как есть. */
|
||
uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0);
|
||
if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */
|
||
/* ...и так же запомнить НЕДОСЧИТАННЫЕ loose: пока карта ещё описывает
|
||
* СТАРУЮ комнату, фаза уезжает в персистентный room_modif + trob, и
|
||
* плита доваливается заочно (см. pop_loose_leave_room / animate_loose). */
|
||
pop_loose_leave_room();
|
||
uint8_t *room_mod;
|
||
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
|
||
pop_map_set(room_fg);
|
||
/* Соседей слева/справа карта забирает из уровня САМА (целиком, см.
|
||
* pop_map_set_edges) — отдельной копии в приложении нет. */
|
||
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
|
||
pop_room_link(room, 2), pop_room_link(room, 3));
|
||
/* Боковой переход: колонки сдвинулись ровно на 10 (x ∓140), флаги
|
||
* перекрытия при этом инвариантны — историю перенумеровываем, а не
|
||
* теряем. Без этого решётка шва не бампит на кадре входа и Кид с
|
||
* разбега проходит сквозь закрытые ворота (BUG-GATE-PASS-1). */
|
||
if (side == 0) pop_coll_shift(+10); /* ушёл ВЛЕВО: col += 10 */
|
||
else if (side == 1) pop_coll_shift(-10); /* ушёл ВПРАВО: col −= 10 */
|
||
pop_room_set_below(below_fg);
|
||
/* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика
|
||
* уровня, mod — живой room_modif (ленивая инициализация). */
|
||
{
|
||
uint8_t up = pop_room_link(room, 2), c;
|
||
if (up) {
|
||
uint8_t *um = pop_trob_modif(up);
|
||
pop_level_access_begin();
|
||
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
|
||
pop_level_access_end();
|
||
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
|
||
pop_room_set_above(above_fg, above_mod);
|
||
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
|
||
} else {
|
||
pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */
|
||
pop_map_set_above(0);
|
||
}
|
||
}
|
||
pop_map_set_room(room);
|
||
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
|
||
* (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
|
||
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
|
||
* стража никогда не срабатывает. */
|
||
Kid.room = room;
|
||
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
|
||
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
|
||
pop_bake_forget(); /* и слоты «первая запечка была на странице N» */
|
||
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
|
||
pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */
|
||
pop_check_fall_flo(); /* спецсобытие ур.13: гряда плит сверху */
|
||
/* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»-
|
||
* уровнях забирает слот соперника себе: вернула 1 — стража поднимать
|
||
* НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда
|
||
* не пускает). Порядок ровно как в оригинале, не «сначала страж, потом
|
||
* заменим». */
|
||
if (!follow && !pop_check_shadow(room))
|
||
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
|
||
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
|
||
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
||
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
||
* а не по свежему backtable. Фаза P0. */
|
||
room_mod = pop_trob_modif(room);
|
||
/* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот,
|
||
* видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6).
|
||
* pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */
|
||
{
|
||
uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1);
|
||
if (lft) { uint8_t *m = pop_trob_modif(lft), i;
|
||
for (i = 0; i < 3; i++) {
|
||
lcol_bg[i] = m[9 + i * 10];
|
||
/* шов рисуется в обе страницы ниже — синхр. сигнатуру */
|
||
seam_sig[i] = seam_row_sig(m, lcol_fg, i);
|
||
} }
|
||
if (rgt) { uint8_t *m = pop_trob_modif(rgt);
|
||
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; }
|
||
}
|
||
seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */
|
||
/* ENTER-ROOM-FAST: комната рисуется ОДИН раз — в скрытую страницу, — и
|
||
* тут же показывается флипом; вторая страница дабл-буфера получает её
|
||
* КОПИЕЙ акселератора (gfx_copy_page, ~384 burst-строки против почти
|
||
* полумиллиона тактов на вторую отрисовку). Побочный эффект важнее
|
||
* тактов: игрок больше не видит, КАК рисуются тайлы — только готовый
|
||
* кадр.
|
||
*
|
||
* Копия идёт банком 0x50, то есть обновляет и VRAM, и ОЗУ-копию
|
||
* страницы-приёмника — иначе heal на ней восстанавливал бы фон прошлой
|
||
* комнаты (инвариант gfx_copy_page, регресс tests/pageflip).
|
||
*
|
||
* Видимая страница здесь МЕНЯЕТСЯ, поэтому главный цикл перечитывает её
|
||
* в начале каждого кадра (front = gfx_get_visible_page()), а не ведёт
|
||
* свой счёт. */
|
||
{
|
||
gfx_rect_t full;
|
||
pg = (uint8_t)(gfx_get_visible_page() ^ 1); /* скрытая */
|
||
gfx_set_draw_page(pg);
|
||
setfillstyle(SOLID_FILL, BLACK);
|
||
bar(0, 0, 319, 255);
|
||
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
|
||
gfx_set_visible_page(pg); /* показать готовое */
|
||
gfx_set_draw_page((uint8_t)(pg ^ 1));
|
||
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
|
||
gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */
|
||
}
|
||
cur_room = room;
|
||
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
|
||
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
|
||
}
|
||
|
||
/* Вход в комнату БЕЗ перехода за Кидом: старт уровня, респавн, чит-обход.
|
||
* Страж такой смены не «видит» — он остаётся в своей комнате. */
|
||
void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); }
|
||
|
||
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
|
||
* (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и
|
||
* для запуска, и для рестарта после смерти / выпадения из уровня: в
|
||
* оригинале это буквально один и тот же путь (play_level_2 зовёт
|
||
* start_level заново), поэтому и у нас не три копии четырёх строк.
|
||
*
|
||
* Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define:
|
||
* start_pos — тайл 0..29 (row*10+col), start_dir — направление, в котором
|
||
* персонаж должен ОКАЗАТЬСЯ.
|
||
*
|
||
* Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж
|
||
* ставится спиной и доворачивается кадрами позы входа. Сама поза — из
|
||
* tbl_entry_pose (data.h:848 = {0,1,0,…,2,…}):
|
||
* 1 (уровень 1) — падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2);
|
||
* это оно захлопывает решётку за спиной в первой комнате;
|
||
* 2 (уровень 13) — вбегание (seq_84_run);
|
||
* иначе — разворот на месте (seq_5_turn).
|
||
* Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */
|
||
#define SEQ_TURN 5
|
||
#define SEQ_FALL 7
|
||
#define SEQ_RUN 84
|
||
#define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */
|
||
#define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */
|
||
#define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */
|
||
|
||
|
||
/* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) -----------------------
|
||
*
|
||
* В оригинале никто не «поверх» по определению: и Кид, и страж попадают в
|
||
* midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а
|
||
* тайлы обходятся в фиксированном порядке — redraw_needed_tiles (seg008:1B06)
|
||
* идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ — тот
|
||
* поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или
|
||
* выше, его тайл в обходе позже. Совпали тайлы — решает sort_curr_objs
|
||
* (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже.
|
||
*
|
||
* Мы рисовали страж→Кид всегда, то есть Кид был поверх безусловно.
|
||
*
|
||
* Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 → (2 - row). */
|
||
#define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col))
|
||
|
||
/* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */
|
||
static int8_t tile_div_mod(int x)
|
||
{
|
||
int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */
|
||
int c = v / TILE_SIZEX;
|
||
if (v % TILE_SIZEX < 0) --c;
|
||
return (int8_t)c;
|
||
}
|
||
|
||
/* char_x_left из set_char_collision (seg006:0723) — левая граница габарита.
|
||
* ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к
|
||
* *_coll, а колонки char_col_left/right считает по этой. */
|
||
static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx,
|
||
uint8_t flags, uint16_t w)
|
||
{
|
||
int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */
|
||
uint8_t fl = flags;
|
||
if (dir < 0) fl ^= 0x80;
|
||
/* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */
|
||
if (!(fl & 0x80) && e <= 57) e++;
|
||
if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */
|
||
return e;
|
||
}
|
||
|
||
/* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа.
|
||
*
|
||
* Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил
|
||
* сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ
|
||
* колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому
|
||
* бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров
|
||
* бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих:
|
||
* enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт
|
||
* дал бы перекос в другую сторону. */
|
||
static void objtile_at_char(int8_t *row, int8_t *col,
|
||
const pop_char_t *ch, int8_t dx, int8_t dy,
|
||
uint8_t flags, uint16_t w)
|
||
{
|
||
int8_t r = ch->curr_row, c = ch->curr_col;
|
||
if (ch->action == 1) { /* actions_1_run_jump */
|
||
int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy));
|
||
if (br == -1) br = 3; /* char_bottom_row */
|
||
r = br;
|
||
c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w));
|
||
if (c < 0) c = 0; /* MAX(char_col_left, 0) */
|
||
}
|
||
if ((ch->frame >= 135 && ch->frame < 149) ||
|
||
ch->action == 2 || ch->action == 3 ||
|
||
ch->action == 4 || ch->action == 6) c--;
|
||
*row = r; *col = c;
|
||
}
|
||
|
||
/* ВРЕМЕННО: трасса kidobj — порт отладочного вывода, добавленного
|
||
* пользователем в SDLPoP add_kid_to_objtable (seg008:1679):
|
||
*
|
||
* DBG kidobj tilepos=%d frame=%d act=%d col=%d cols=%d..%d rows=%d..%d
|
||
*
|
||
* Считаем ровно то же, что set_char_collision (seg006:0723) +
|
||
* set_objtile_at_char (seg006:13F3): габарит НЕутоньшённый (THIN правит
|
||
* только *_coll, а колонки берутся по «толстой» границе), колонки
|
||
* клампятся в 0..9, нижний ряд −1 читается как 3. Звать ПОСЛЕ отрисовки
|
||
* Кида: ширину/высоту кадра (fpw/fph) заполняет она.
|
||
*
|
||
* Вывод забирает брейкпоинт MAME на резидентном pop_dbg_trap — сама
|
||
* функция банковая, её адрес в W3 неоднозначен. */
|
||
void pop_dbg_kidobj(void) __banked
|
||
{
|
||
int8_t dx = kid_cur_dx(), dy = kid_cur_dy();
|
||
uint8_t fl = kid_cur_flags();
|
||
uint16_t w = pop_cd[POP_CH_KID].fpw;
|
||
uint16_t h = pop_cd[POP_CH_KID].fph;
|
||
int xl = char_x_left_of(Kid.x, Kid.direction, dx, fl, w);
|
||
int xr = xl + (int)((w + 1) >> 1); /* + char_width_half */
|
||
int oy = (int)Kid.y + dy;
|
||
int ty = oy - (int)h + 1;
|
||
int8_t r, c, tr, br, cl, cr;
|
||
objtile_at_char(&r, &c, &Kid, dx, dy, fl, w);
|
||
br = pop_y_to_row((int16_t)oy);
|
||
if (br == -1) br = 3;
|
||
if (ty >= 192) ty = 0;
|
||
tr = pop_y_to_row((int16_t)ty);
|
||
cl = tile_div_mod(xl); if (cl < 0) cl = 0;
|
||
cr = tile_div_mod(xr); if (cr > 9) cr = 9;
|
||
pop_dbg_obj[0] = (int8_t)(r * 10 + c);
|
||
pop_dbg_obj[1] = (int8_t)Kid.frame;
|
||
pop_dbg_obj[2] = (int8_t)Kid.action;
|
||
pop_dbg_obj[3] = Kid.curr_col;
|
||
pop_dbg_obj[4] = cl;
|
||
pop_dbg_obj[5] = cr;
|
||
pop_dbg_obj[6] = tr;
|
||
pop_dbg_obj[7] = br;
|
||
pop_dbg_obj[8] = (int8_t)cur_room;
|
||
pop_dbg_trap();
|
||
}
|
||
|
||
/* 1 = страж рисуется ПОСЛЕ Кида (поверх). __banked: зовётся раз в кадр,
|
||
* то есть один трамплин — а 500 Б кода в резиденте дороже. */
|
||
uint8_t guard_over_kid(void) __banked
|
||
{
|
||
int8_t kr, kc, gr, gc;
|
||
int kk, gk;
|
||
if (Guard.charid == 0 || Guard.room != cur_room) return 0;
|
||
objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(),
|
||
kid_cur_flags(), pop_cd[POP_CH_KID].fpw);
|
||
objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy,
|
||
pop_gframe.flags, pop_cd[POP_CH_OPP].fpw);
|
||
kk = OBJ_ORDER_KEY(kr, kc);
|
||
gk = OBJ_ORDER_KEY(gr, gc);
|
||
if (gk != kk) return (uint8_t)(gk > kk);
|
||
/* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */
|
||
return (uint8_t)((int)Guard.y + pop_gframe.dy > pop_cd[POP_CH_KID].fpy);
|
||
}
|
||
|
||
|
||
/* ---- Переворот уже нарисованного (зелье инверсии, уровень 9) --------- *
|
||
*
|
||
* Порт need_redraw_because_flipped (seg000:15E9 + 939/946). Оригинал
|
||
* держит готовый offscreen и переворачивает его на выводе; у нас страниц
|
||
* ровно две, рабочей третьей нет, поэтому переворачиваем то, что уже
|
||
* нарисовано, — построчной копией акселератора (gfx_copy_page).
|
||
*
|
||
* Почему это законно и почему heal после не ломается: accel ЧИТАЕТ
|
||
* ОЗУ-копию, значит копируется ЧИСТЫЙ ФОН без персонажей, а пишет банком
|
||
* 0x50 — и в видео, и в копию приёмника. После двух проходов обе страницы
|
||
* дабл-буфера содержат перевёрнутый фон в обеих плоскостях, и heal
|
||
* восстанавливает уже его (memory accel_port_y_between_triggers,
|
||
* регресс — tests/pageflip).
|
||
*
|
||
* Порядок: перевернуть в СКРЫТУЮ, показать её, скопировать оттуда во
|
||
* вторую (теперь скрытую) — прямой копией. Так игрок видит результат
|
||
* сразу, а не после второго прохода.
|
||
*
|
||
* Переворачиваем ТОЛЬКО игровое поле (POP_PLAYFIELD_H): полоса HP лежит
|
||
* ниже него и в оригинале тоже не переворачивается. */
|
||
void pop_flip_screen(void) __banked
|
||
{
|
||
gfx_rect_t area;
|
||
uint8_t front = gfx_get_visible_page();
|
||
uint8_t back = (uint8_t)(front ^ 1);
|
||
uint8_t i;
|
||
|
||
area.x = 0; area.y = POP_YOFF;
|
||
area.w = 320; area.h = POP_PLAYFIELD_H;
|
||
|
||
/* СНАЧАЛА убрать из фона то, что в нём запечено, но фоном не является:
|
||
* пламя факелов (pop_torch_wipe объясняет, почему оно вообще там).
|
||
* Отражённое вместе с фоном, оно оседало в зеркальных позициях, где его
|
||
* никто не перерисовывает — по комнате оставались лишние языки огня
|
||
* (нашёл пользователь на глотке зелья).
|
||
*
|
||
* Чистим ОБЕ страницы: источник отражения — видимая, а вторая получит
|
||
* прямую копию уже чистой картинки. Дешевле полной перерисовки комнаты
|
||
* на порядок: 0..4 тайла против 6.2 млн тактов (14.5 кадра, замер MAME
|
||
* 2026-08-12) — а копия отражения стоит всего 2.1 кадра. */
|
||
/* ВНИМАНИЕ на систему координат: pop_upside к этому моменту УЖЕ
|
||
* переключён (главный цикл делает это перед вызовом), а картинка на
|
||
* экране нарисована ещё в СТАРОЙ. Чистить её надо по старым
|
||
* координатам, иначе чёрный бар ложится в зеркальное место — на экране
|
||
* оставался чёрный квадрат, а пламя так и не стиралось. */
|
||
pop_upside = (uint8_t)!pop_upside; /* временно: система ЭКРАНА */
|
||
gfx_set_draw_page(front);
|
||
for (i = 0; i < 30; i++) {
|
||
uint8_t t = room_fg[i];
|
||
if (t == 19 || t == 30) /* факел / факел с щебнем */
|
||
pop_torch_wipe(i / 10, i % 10);
|
||
}
|
||
pop_upside = (uint8_t)!pop_upside; /* вернуть новое состояние */
|
||
|
||
gfx_set_draw_page(back);
|
||
gfx_copy_page(&area, GFX_COPY_VFLIP); /* источник — видимая страница */
|
||
gfx_set_visible_page(back);
|
||
|
||
gfx_set_draw_page(front);
|
||
gfx_copy_page(&area, GFX_COPY_DIRECT); /* вторую страницу — прямой копией */
|
||
|
||
/* Всё, что рисуется поверх фона, надо положить заново: прошлые
|
||
* прямоугольники персонажей относятся к СТАРОЙ (неперевёрнутой)
|
||
* картинке, и heal по ним стёр бы не то место. */
|
||
pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0;
|
||
pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0;
|
||
pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0;
|
||
pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0;
|
||
POP_CD_TOUCH_ALL(); /* фон трогали целиком */
|
||
pop_hp_invalidate(); /* полосу HP перерисовать */
|
||
}
|
||
|
||
/* ---- Запуск, смена уровня, завершение ------------------------------- *
|
||
* Три куска главного цикла, которые отрабатывают раз за игру (или раз на
|
||
* уровень). В резиденте от них оставались только вызовы — сам код здесь.
|
||
*/
|
||
|
||
int pop_boot(void) __banked
|
||
{
|
||
/* Набор фона — по типу СТАРТОВОГО уровня. Атласы грузятся ДО initgraph
|
||
* (тот снимает DSS-страницу W0); уровень к этому моменту ещё не прочитан,
|
||
* поэтому тип берём напрямую от номера. */
|
||
pop_current_level = FIRST_LEVEL;
|
||
if (pop_bg_load(pop_level_type()) != 0) {
|
||
puts("pop_bg_load failed");
|
||
return -1;
|
||
}
|
||
pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */
|
||
pop_cheats = 1; /* режим разработки: читы включены */
|
||
pop_guard_reset();
|
||
if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
|
||
printf("kid_data.bin load failed\n");
|
||
return -1;
|
||
}
|
||
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
|
||
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
||
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
|
||
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
|
||
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
||
puts("kid atlas load failed");
|
||
return -1;
|
||
}
|
||
if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */
|
||
puts("level load failed");
|
||
return -1;
|
||
}
|
||
pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */
|
||
pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
|
||
pop_checkpoint = 0; /* init_game (seg003:37) */
|
||
|
||
initgraph();
|
||
gfx_set_visible_page(0);
|
||
if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */
|
||
gfx_pal_fload(0, "a:\\kid.pal");
|
||
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
|
||
/* kid.pal — ЕДИНАЯ игровая палитра, собранная из VDUNGEON, и она только
|
||
* что затёрла слоты тайлсета. Возвращаем цвета текущего набора: на
|
||
* дворцовых уровнях без этого рисуется дворцовая геометрия подземельными
|
||
* красками (сине-серое вместо песочного). */
|
||
pop_bg_pal_apply();
|
||
/* ...и палитру Тени: kid.pal затёр и её 16 слотов (pop_shadow.c). */
|
||
pop_shadow_pal_apply();
|
||
|
||
/* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя
|
||
* ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
|
||
* страницы, в которую рисуем. Дальше каждый тик восстанавливаем
|
||
* ТОЛЬКО прямоугольник Кида (pop_char_heal), а не весь экран. */
|
||
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
|
||
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
|
||
/* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
|
||
* pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места,
|
||
* Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */
|
||
pop_start_level();
|
||
/* Зеркальный набор персонажей — ТУТ ЖЕ, если стартуем сразу на уровне с
|
||
* зельем инверсии (make LEVEL=9). Иначе первый переворот утащил бы
|
||
* чтение 34 файлов (~1.5 с) прямо в кадр: пользователь видел «затык» в
|
||
* синей фазе профиля. Тот же вызов есть в смене уровня; функция
|
||
* идемпотентна и звать её можно откуда угодно (pop_vflip.h). */
|
||
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
|
||
gfx_set_draw_page(0);
|
||
pop_char_draw(POP_CH_KID);
|
||
pop_char_fore(POP_CH_KID);
|
||
/* Звук включаем ПОСЛЕДНИМ действием инициализации — когда все атласы,
|
||
* уровень и палитры уже прочитаны. Раньше нельзя: ESTEX уходит в диск
|
||
* надолго, насос не успевает долить блок, и на слух это скрежет
|
||
* (pop_sfx.h). Парная к pop_sfx_pause в pop_level_switch. */
|
||
pop_sfx_start();
|
||
return 0;
|
||
}
|
||
|
||
/* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): цикл
|
||
* уровня кончается, когда next_level разошёлся с current_level (END_LEVEL
|
||
* после seq_70 в двери уровня либо чит Shift+L), и play_level грузит
|
||
* следующий. Возврат -1 — фатально (не удалось сменить набор фона). */
|
||
int pop_level_switch(void) __banked
|
||
{
|
||
|
||
/* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81):
|
||
* цикл уровня кончается, когда next_level разошёлся с
|
||
* current_level (END_LEVEL после seq_70 в двери уровня либо чит
|
||
* Shift+L), и play_level грузит следующий.
|
||
*
|
||
* HP переносится: hitp_beg_lev = hitp_max ДО загрузки — так
|
||
* поднятая на уровне «жизнь» (большая склянка) переезжает
|
||
* дальше, а урон — нет (do_startpos дольёт до потолка). */
|
||
hitp_beg_lev = hitp_max;
|
||
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
|
||
pop_sfx_pause(); /* загрузка уровня — ESTEX надолго, звук глушим */
|
||
if (pop_level_load_num(pop_next_level) != 0) {
|
||
pop_next_level = pop_current_level; /* файла нет — остаёмся */
|
||
} else {
|
||
/* Набор спрайтов соперника у каждого уровня СВОЙ
|
||
* (load_lev_spr, seg000:1092): уровень 3 — скелет, у него и
|
||
* кадров меньше, чем у стража. Без перезагрузки соперник
|
||
* рисуется чужим атласом: часть индексов уходит за его
|
||
* пределы, и персонаж просто не виден. */
|
||
pop_guard_free();
|
||
pop_guard_load();
|
||
/* Зеркальные копии атласов сделаны из СТАРЫХ страниц —
|
||
* после перезагрузки спрайтов они мусор (pop_vflip.h). */
|
||
pop_vflip_reset();
|
||
/* Тайлсет: подземелье <-> дворец (порт tbl_envir_ki,
|
||
* seg000:1108). Внутри — no-op, если тип не менялся;
|
||
* при смене выгружает старые атласы и правит 32 записи
|
||
* палитры. Только на границе уровня, не в кадре. */
|
||
if (pop_bg_load(pop_level_type()) != 0) {
|
||
puts("bg set load failed");
|
||
return -1;
|
||
}
|
||
/* Всё состояние прошлого уровня сносит сам pop_start_level:
|
||
* pop_trob_reset обнуляет room_seen (модификаторы комнат
|
||
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
|
||
* из неё же, loose чистит enter_room. */
|
||
pop_start_level();
|
||
/* Уровень с зельем инверсии — заранее поднять зеркальный
|
||
* набор персонажей (pop_vflip.h): 34 страницы читаются ~1.5 с,
|
||
* и делать это в момент глотка нельзя. Звать можно откуда
|
||
* угодно — функция идемпотентна. */
|
||
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
|
||
pop_sfx_start(); /* ...и обратно, когда всё прочитано */
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
void pop_shutdown(void) __banked
|
||
{
|
||
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
|
||
/* ЗВУК ГАСИМ ПЕРВЫМ, до closegraph и освобождения страниц. Дальше идут
|
||
* ESTEX-вызовы (mem_free_block на каждый атлас), а они уходят надолго —
|
||
* насос не успевает долить очередной блок, и железо доигрывает по кругу
|
||
* несвежую половину буфера. Замер записи MAME: на выходе по ESC ровно
|
||
* 11 мс шума на полной громкости, то есть ОДИН пропущенный блок (128
|
||
* сэмплов = 11,7 мс). Та же причина, по которой звук глушится на время
|
||
* загрузки уровня (pop_sfx.h). */
|
||
pop_sfx_close();
|
||
pop_ctrl_close();
|
||
closegraph();
|
||
pop_bg_free();
|
||
pop_kid_free();
|
||
pop_shadow_free();
|
||
pop_guard_free();
|
||
pop_kid_data_free();
|
||
pop_level_free();
|
||
}
|