Files
Sprinter-SDCC/applications/PoP/roomtest/pop_map.c
T
snark13 6d7c1c8b6b P5: гейты холостого хода в pop_loose_tick — минус 33 840 тактов на кадре
Замер 11/15 показал, что loose-механика берёт 28 872 такта в комнате, где
не анимируется ни одна плита и не летит ни один кусок.  Раскладка зондами
m9..m12: два цикла по тайлам 9 852, обход 14 слотов mob 12 090, поиск
куска над головой Кида 5 868 (там ещё и банковый трамплин).

Два гейта:

  loose_any (статик pop_map.c) — «идёт ли анимация плит».  Ставят пять мест
  записи ненулевой фазы: make_loose_fall, ветка потолка в check_press,
  do_knock для обоих рядов и восстановление фазы из room_modif при входе в
  комнату.  Снимает его сам цикл, по факту прохода, в котором не осталось
  ни одной живой фазы.

  pop_mob_busy (резидент pop_state.c) — «занят ли слот падающего куска»
  (active или дочистка clean).  Ставит mob_alloc, снимает обход по факту
  пустой таблицы.  В резиденте, а не в pop_room.c, потому что читает его
  pop_map из банка 3, а писучие статики банкового модуля наружу не видны.

Гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация»: у
лежащей плиты-потолка фаза нулевая, и крутить нечего (вопрос пользователя).
Асимметрия намеренная — ложная единица стоит одного холостого прохода,
ложный ноль стоит застывшей навсегда плиты, поэтому взвод стоит рядом с
КАЖДОЙ записью, а снятие только по факту пустого прохода.

Стало: 132 / 996 / 546, вся функция 28 872 -> 2 760.  На кадре работа
801 768 -> 767 928.  Ожидание по реестру было -28 000.

Покрытие: новый phys_loose_gate_survives_room_change на пятое место взвода
(фаза восстановлена входом в комнату) — единственное, которое не прогонял
ни один тест, и дающее самый тихий отказ.  Мутационная проверка: со снятым
взводом тест падает (фаза 3 вместо 4).

Заодно отладочный старт сразу в целевую комнату: make ROOM=15 POS=2
(дефолт), roomtest стартует в 11/15 с Кидом в (0,2).  kid_init ставит
x = x_bump[col] + TILE_SIZEX, а это левая граница СЛЕДУЮЩЕЙ колонки — с неё
физика относила Кида в тайл чомпера, и он погибал на старте (найдено
пользователем).  Сдвиг внутрь на 2: колонку определяет весовая точка кадра,
поэтому число снято замером, а не выведено геометрией.

План работ между сессиями — docs/perf_registry.md §4: очередь позиций со
статусами, текущий бюджет сцены, рецепт её воспроизведения и метод замера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:49:19 +03:00

3237 lines
196 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_map.c — коллизия Kid с картой (Фаза K3.2: падение). Порт SDLPoP
* seg005/seg006. Пер-кадровый physics-диспетч check_action() по Char.action:
* grounded (stand/run/step/crouch) -> check_on_floor(): кадр с флагом
* FRAME_NEEDS_FLOOR (0x40), под персонажем не floor -> start_fall();
* стена -> in_wall();
* freefall (action 4) -> do_fall(): fall_y-ускорение (в fall_accel/speed),
* достиг pop_y_land[curr_row+1] -> land()/inc_curr_row.
*
* Координатная система PoP: экран.left = 58, тайл = 14 ед. (TILE_SIZEX);
* col = (xpos-58)/14 (округл. вниз), xl = остаток; get_tile_div_mod_m7 =
* div_mod(xpos-7). Карта: fg[row*10+col] & 0x1F; col<0/>9,row<0/>2 = стена.
*
* MVP: без spike/HP/grab/gate — падение + мягкое/среднее приземление.
* Бег В стену (check_bumped) и осторожный шаг у края (get_edge_distance) —
* следующим шагом.
*/
#include <stdint.h>
#include <string.h> /* memcpy = LDIR: цикл на C тут стоил ~390 тактов на байт */
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
#include "pop_geom.h" /* общая геометрия (x_bump/y_land/y_to_row) */
#include "pop_tile.h" /* POP_TILE_DIV/MOD — деление на ширину тайла таблицей */
#include "pop_redraw.h" /* пометки перерисовки (порт set_redraw_*) */
/* pop_bg нужен ТОЛЬКО падающему куску (mob): spawn/tick/pos — это движущийся
* ОБЪЕКТ, а не перерисовка тайла (в оригинале он и живёт отдельно: mobs +
* draw_moving). Из-за него pop_loose_tick — единственная функция модуля,
* которую нельзя звать из __banked-кода; вся коллизия/физика/предметы, то
* есть то, что понадобится стражам, чиста от графики. Разделение mob на
* логику и отрисовку — задел следующей фазы. */
#include "pop_bg.h"
#include "pop_state.h"
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
#include "pop_level.h" /* pop_level_start_room — вход в дверь уровня */
#include "pop_guard.h" /* SWORD_2_DRAWN — ветка приземления с мечом */
#include "pop_tune.h" /* POP_ENABLE_JUMP_GRAB и прочие настройки */
#include "pop_cdraw.h" /* габарит кадра персонажа (pop_cd) */
/* Габарит последнего нарисованного кадра АКТИВНОГО персонажа (char_width_half
* и char_top_y в set_char_collision, seg006:0723). У каждого слота он свой:
* до DRAW-CHAR отрисовка была продублирована, метрики Кида лежали отдельно, и
* коллизия СТРАЖА считалась по ширине спрайта КИДА. Отставание на кадр —
* как в оригинале (cur_frame берётся тем же кадром, что рисуется). */
#define CD_ACT (&pop_cd[Char.charid == CHARID_0_KID ? POP_CH_KID : POP_CH_OPP])
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
#define TILE_EMPTY 0
#define TILE_GATE 4
#define TILE_DOORTOP_FLOOR 7 /* tapestry (пол сверху) */
#define TILE_DOORTOP 12 /* tapestry top */
#define TILE_MIRROR 13
#define TILE_CHOMPER 18
#define TILE_WALL 20
#define TILE_LOOSE 11 /* проваливающийся пол */
#define TILE_DEBRIS 14 /* щебень (упавший loose) */
#define TILE_TORCH 19 /* факел */
#define TILE_TORCH_DEBRIS 30 /* факел + щебень (loose_land) */
#define TILE_SPIKE 2 /* пики */
#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */
#define TILE_STUCK 5 /* заклиненная кнопка (died_on_button) */
#define TILE_POTION 10 /* зелье */
#define TILE_SWORD 22 /* меч на полу */
#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */
#define TILE_LEVELDOOR_LEFT 16 /* ЛЕВАЯ половина двери уровня (на ней modif) */
#define ACT_STAND 0
#define ACT_RUNJUMP 1
#define ACT_HANGCLIMB 2
#define ACT_MIDAIR 3
#define ACT_FREEFALL 4
#define ACT_BUMPED 5
#define ACT_HANGSTRAIGHT 6
#define ACT_TURN 7
#define SEQ_7_FALL 7 /* stepfall (stand/run/step/crouch) */
#define SEQ_17_SOFT_LAND 17
#define SEQ_18_FALL_STANDJUMP 18
#define SEQ_19_FALL 19 /* stepfall2 (run frames 9/13) */
#define SEQ_20_MEDIUM_LAND 20
#define SEQ_21_FALL_RUNJUMP 21
#define SEQ_45_BUMPFALL 45 /* удар В ВОЗДУХЕ: отскок и падение */
#define SEQ_46_HARDBUMP 46 /* удар после прыжка: отскок + ПРИСЕДАНИЕ */
#define SEQ_47_BUMP 47 /* удар в стену: dx(-4) отскок + 50-52 + stand */
/* K4 — зацеп/вис/подтягивание/спуск */
#define SEQ_8_GRAB_STRAIGHT 8 /* прыжок вверх и зацеп (прямо) */
#define SEQ_9_GRAB_WHILE_JUMPING 9 /* зацеп ПРЯМО В ПРЫЖКЕ (улучшение SDLPoP) */
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
#define SEQ_16_JUMP_UP_AND_GRAB 16 /* прыжок вверх и зацеп (нет пола сзади)*/
#define SEQ_24_GRAB_FORWARD 24 /* прыжок вверх и зацеп (пол сзади) */
#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */
#define SEQ_10_CLIMB_UP 10 /* подтягивание из виса */
#define SEQ_11_RELEASE_LEDGE_LAND 11 /* сорваться и встать (конец спуска) */
#define SEQ_15_GRAB_LEDGE_MIDAIR 15 /* зацеп за уступ в падении */
#define SEQ_23_RELEASE_LEDGE_FALL 23 /* отпустить уступ и падать */
#define SEQ_68_CLIMB_DOWN 68 /* спуск через зацеп (не приседание) */
#define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */
#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */
#define SEQ_54_CHOMPED 54 /* попасть в челюсти чомпера (смерть) */
#define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */
#define SEQ_63_ACTIVE_AFTER_FALL 63 /* стойка С МЕЧОМ после приземления */
#define SEQ_65_BUMP_FWD_SWORD 65 /* стража прижали к стене (с мечом) */
#define SEQ_81_FIGHTFALL 81 /* fightfall: падение из боевой стойки */
#define SEQ_82_GUARD_PUSHED_OFF 82 /* стража спихнули с уступа */
#define SEQ_83_GUARD_FALL 83 /* страж сам шагнул в провал с мечом */
#define TILE_FLOOR 1
#define FRAME_109_CROUCH 109
#define FRAME_178_CHOMPED 178
/* Кадры анимации для check_spiked (seg006:0658). */
#define FRAME_7_RUN 7
#define FRAME_26_STANDJUMP_LAND 26
#define FRAME_34_RUNJUMP_START 34
#define FRAME_43_RUNJUMP_LAND 43
#define FRAME_NEEDS_FLOOR 0x40
#define FRAME_WEIGHT_X 0x1F
#define TILE_RIGHTX 13
#define SCREENSPACE_X 58
#define FALL_ACCEL 3
#define FALL_MAX 33
/* Перо (seg006:057C, types.h:1435): ускорение и потолок скорости падения
* под зельем медленного падения. */
#define FALL_ACCEL_FEATHER 1
#define FALL_MAX_FEATHER 4
#define TILE_MIDX 7
#define FRAME_THIN 0x20
static const int8_t dir_front[2] = {-1, 1}; /* [dir+1]: dir=-1->-1, 0->+1 */
static const int8_t dir_behind[2] = {1, -1}; /* [dir+1]: назад по направлению */
/* wall_type(tile): gate/doortop=1, mirror=2, chomper=3, wall=4, иначе 0. */
static const int8_t wall_dl[6] = {0, 10, 0, -1, 0, 0}; /* wall_dist_from_left */
static const int8_t wall_dr[6] = {0, 0, 10, 13, 0, 0}; /* wall_dist_from_right */
static int8_t g_infrontx; /* колонка «перед персонажем» (set в get_tile_infrontof) */
/* ---- Карта комнаты ------------------------------------------------- */
uint8_t pop_fell_out; /* 1 = Kid пересёк НИЖНЮЮ границу (см. pop_map.h) */
uint8_t pop_leave_dir; /* уход вбок/вверх: 0=нет; 1=left,2=right,3=up (side+1) */
uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заблокирован N кадров после входа в комнату */
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
* Смерть/рестарт уровня возвращают ровно его (do_startpos: hitp_max =
* hitp_curr = hitp_beg_lev), а по ПРОХОЖДЕНИИ уровня он подтягивается к
* достигнутому максимуму (hitp_beg_lev = hitp_max) — так «жизнь» из большой
* склянки переносится на следующий уровень, а из мёртвых прогонов — нет.
* Стартовое значение (custom->start_hitp = 3) ставит main через
* pop_kid_hp_newgame — инициализатором нельзя: модуль банкованный. */
uint8_t hitp_beg_lev;
/* Дельта HP Кида за кадр (порт hitp_delta). Её ставят боёвка (guards.c) и
* физика (пики/падение/плита на голову — через общий pop_take_hp), а
* применяет pop_do_delta_hp раз в кадр, как в оригинале. Раньше путь
* пик/падения правил hitp_curr напрямую; с переходом физики на Char это
* стало невозможно — урон должен уметь уходить и стражу. */
int8_t hitp_delta;
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
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_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
/* is_feather_fall (seg000:15F8): кадров, пока действует зелье медленного
* падения. У оригинала это флаг + счётчик тиков со сроком по звуку; у нас
* просто обратный отсчёт кадров (POP_FEATHER_FRAMES, pop_tune.h). Публичный:
* его читают play_seq (опкод JMP_IF_FEATHER, pop_kid.c) и приложение. */
uint8_t pop_feather;
/* upside_down (seg000:15E9 toggle_upside): 1 — картинка перевёрнута вверх
* ногами (зелье типа 4, уровень 9). Держим ЗДЕСЬ, рядом с пером: оба
* ставятся из одной ветки proc_get_object и оба сбрасываются стартом
* уровня. Читает отрисовка (банки 2/4/7) и главный цикл — отсюда
* публичность. Смерть Кида снимает эффект (seg000:1224). */
uint8_t pop_upside;
/* ЖЕЛАЕМОЕ состояние переворота. Зелье и смерть Кида пишут только сюда, а
* pop_upside переключает главный цикл — ОДИН раз, в начале кадра, вместе с
* перерисовкой (pop_flip_screen).
*
* Зачем разделение. Зелье выпивается из play_seq, то есть В СЕРЕДИНЕ кадра:
* переключи флаг сразу — и остаток кадра рисуется уже зеркально поверх ещё
* неперевёрнутого фона. Хуже всего пламя факела: оно ЗАПЕКАЕТСЯ в ОЗУ-копию
* (pop_torch_draw), поэтому кадр, положенный в зеркальную позицию на старом
* фоне, оставался там навсегда — по комнате рассыпались лишние языки огня
* (нашёл пользователь на глотке зелья; чит U этим не страдал, потому что
* стоит в начале кадра). */
uint8_t pop_upside_want;
/* need_redraw_because_flipped (seg000:15E9/1226): состояние ОТЛИЧАЕТСЯ от
* желаемого — главный цикл применит его и снимет флаг. */
uint8_t pop_upside_dirty;
/* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам
* (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом
* с check_collisions. Сброс при смене комнаты — coll_prev_row. */
static void pop_coll_invalidate(void);
/* Связи комнаты (0 = нет соседа) + ЦЕЛИКОМ fg соседей слева и справа
* (g_lcol/g_rcol, раскладка комнаты [row*10 + col]). Оригинал резолвит
* любую колонку через find_room_of_tile (seg006:005D), гуляя по roomlinks;
* у нас уровень лежит в EMM-странице, маппить её на каждый get_tile (горячий
* путь физики) дорого — поэтому соседи кэшируются на входе в комнату.
*
* Почему именно ВСЯ комната, а не кромочные колонки: col считается из x
* персонажа (floor((x 7 58)/14)), и на всём диапазоне x 0..255 даёт
* col от −5 до 13. Кромок не хватало, а «за кэшем» get_tile отдавал стену —
* мнимую. Так пропадала видимость стража (BUG-SWORD-GHOST-1: страж после
* follow_guard оказывался в col 12, луч упирался в мнимую стену, Кид прятал
* меч и не мог достать обратно) и так же ломались коллизия/разворот у швов.
* Задаются pop_map_set_edges. */
static uint8_t g_link_l, g_link_r, g_link_u, g_link_d;
static uint8_t g_lcol[30], g_rcol[30];
/* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- *
* В оригинале это тот же loose, просто get_tile ушёл в комнату сверху
* (find_room_of_tile) и tilepos получился отрицательным (set_redraw* уводит
* такие в redraw_frames_above). У нас ряд 2 верхнего соседа живёт отдельной
* копией (above_fg в приложении, наш «ряд −1»), поэтому и состояние
* отдельное — pop_ceil_modif[col] с той же семантикой (0 покой, 0x80.. —
* тряска, 1..11 — отсчёт до падения). */
uint8_t pop_ceil_modif[10]; /* публично: читает pop_bg при отрисовке */
static uint8_t *g_above; /* ряд 2 комнаты сверху (fg), мутабельный */
uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провалившейся плиты-потолка */
uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */
void pop_map_set_above(uint8_t *above_row2_fg) __banked { g_above = above_row2_fg; }
void pop_map_set(uint8_t *fg) __banked { g_fg = fg; }
void pop_map_set_room(uint8_t room) __banked { g_room = room; }
/* do_startpos (seg003:0160): HP уровня = hitp_beg_lev, а не константа —
* иначе большая склянка теряется на границе уровня. */
void pop_kid_hp_reset(void) __banked
{
hitp_curr = hitp_max = hitp_beg_lev;
hitp_delta = 0;
pop_kid_dead = 0;
}
/* Бесшовный переход (seamless, seg003:158): уровень стартует, НЕ трогая HP —
* Кид уносит на следующий и потолок, и полученный урон, потому что для него
* это не «новый уровень», а продолжение того же бега. Всё остальное, что
* делает pop_kid_hp_reset, нужно и здесь: дельта кадра и признак смерти. */
void pop_kid_hp_carry(void) __banked
{
hitp_delta = 0;
pop_kid_dead = 0;
}
/* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */
void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; }
/* add_life (seg000:11EC) + set_health_life (seg000:1200): большая склянка
* поднимает ПОТОЛОК HP (до max_hitp_allowed = 10) и доливает до него. */
static void pop_kid_add_life(void)
{
uint8_t hpmax = (uint8_t)(hitp_max + 1);
if (hpmax > POP_MAX_HITP) hpmax = POP_MAX_HITP;
hitp_max = hpmax;
hitp_delta = (int8_t)(hitp_max - hitp_curr);
}
/* Тот же add_life наружу: слияние с тенью (уровень 12) даёт ровно ту же
* прибавку, что большая склянка, а живёт ИИ тени в другом банке. */
void pop_add_life(void) __banked { pop_kid_add_life(); }
/* Урон — через общий pop_take_hp (резидент pop_guard.c): физика работает
* над Char, значит и HP обязаны убавляться у ТОГО, кто сейчас в Char. Своя
* копия take_hp здесь была Kid-only и правила hitp_curr мимо дельты. */
/* Перечитать fg соседей из уровня, НЕ трогая историю коллизий. Отдельно от
* pop_map_set_edges, потому что нужен и посреди жизни в комнате: тайл соседа
* может измениться, пока тот не отрисован (плита, брошенная Кидом на
* полпути, доваливается заочно — animate_loose в pop_trob.c). g_lcol/g_rcol
* до этого были снимком НА МОМЕНТ ВХОДА, и коллизия у шва продолжала считать
* провалившуюся плиту полом. */
void pop_map_refresh_edges(void) __banked
{
uint8_t i;
if (!g_link_l && !g_link_r) return;
pop_level_access_begin();
for (i = 0; i < 30; i++) {
if (g_link_l) g_lcol[i] = pop_level_tile_raw(g_link_l, i);
if (g_link_r) g_rcol[i] = pop_level_tile_raw(g_link_r, i);
}
pop_level_access_end();
}
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked
{
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
/* fg соседей — прямо из уровня, одной скобкой доступа на обе комнаты.
* Держать ещё одну копию в приложении незачем: карта — единственный её
* потребитель (отрисовке швов хватает кромочных колонок pop_room_load). */
pop_map_refresh_edges();
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и
* бампа на кадре входа нет). БОКОВОЙ переход это отменяет: вызывающий
* сразу зовёт pop_coll_shift(∓10) и историю не теряет. */
pop_coll_invalidate();
}
static uint8_t get_tile(int8_t col, int8_t row)
{
/* Ряд −1 — ряд 2 комнаты СВЕРХУ (порт find_room_of_tile, seg006:005D:
* отрицательный row уходит по links.up). Нужен для зацепа за край
* провалившейся плиты-потолка (колодец наверх) и выбора seq прыжка. */
if (row == -1 && g_above && g_link_u && col >= 0 && col <= 9)
g_curr_tile = (uint8_t)(g_above[col] & 0x1F);
else if (row < 0 || row > 2)
g_curr_tile = TILE_WALL;
else if (col < 0 && col >= -10 && g_link_l) /* уходим в комнату СЛЕВА */
g_curr_tile = g_lcol[row * 10 + col + 10] & 0x1F;
else if (col > 9 && col <= 19 && g_link_r) /* и в комнату СПРАВА */
g_curr_tile = g_rcol[row * 10 + col - 10] & 0x1F;
else if (col < 0 || col > 9)
g_curr_tile = TILE_WALL; /* край уровня / нет соседа = стена */
else
g_curr_tile = g_fg[row * 10 + col] & 0x1F;
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 не бампит и
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
* tile_is_floor это ПОЛ), поэтому Kid НЕ падает на них. Раньше get_tile ради
* проходимости подменял ворота шва на empty — но подмену видел и check_on_floor
* → start_fall (Kid проваливался при проходе через ОТКРЫТЫЕ ворота шва). Теперь
* проходимость живёт ТОЛЬКО в bump-логике (как в SDLPoP), не трогая проверку пола.
* Модификатор: шов слева (col=-1) — сосед g_link_l col9; шов справа (col=10) —
* g_link_r col0; ВНУТРИ комнаты (0..9) — текущая комната g_room (иначе после
* перехода Kid встаёт на ту же створку уже как на внутрикомнатную и его отбивает
* обратно). Открыто ⇔ (modif>>2)+6 >= char_height (бары выше головы). */
/* Комната и tilepos клетки (col,row) С УЧЁТОМ ШВА — порт того, что в
* оригинале делает сам get_tile: он зовёт find_room_of_tile (seg006:005D) и
* ПЕРЕСТАВЛЯЕТ curr_room/curr_tilepos, а дальше trigger_button работает уже
* с ними, а не с комнатой персонажа. Возвращает 0, если клетки нет.
*
* Кусало 2026-08-11 (нашёл пользователь на уровне 5): Кид стоит на кнопке,
* которая физически в СОСЕДНЕЙ комнате (col = −1, то есть колонка 9 левого
* соседа). Мы читали модификатор как (своя комната, tilepos 9) — там стена
* с bg = 0, то есть индекс связи 0. Цепочка от нуля в комнате 24 уровня 5
* открывает И нижние ворота, И верхние, хотя настоящая кнопка (индекс 9)
* открывает только нижние. */
static uint8_t tile_room_of(int8_t col, int8_t row, uint8_t *tp)
{
if (row < 0 || row > 2) return 0;
if (col >= 0 && col <= 9) { *tp = (uint8_t)(row * 10 + col); return g_room; }
if (col < 0 && col >= -10 && g_link_l) { *tp = (uint8_t)(row * 10 + col + 10); return g_link_l; }
if (col > 9 && col <= 19 && g_link_r) { *tp = (uint8_t)(row * 10 + col - 10); return g_link_r; }
return 0;
}
/* Живой модификатор (openness) ворот в (col,row): своя комната или соседняя
* через шов. 0xFF — тайла нет (за краем уровня). */
static uint8_t gate_modif(int8_t col, int8_t row)
{
if (row < 0 || row > 2) return 0xFF;
if (col >= 0 && col <= 9) return pop_trob_modif(g_room)[row * 10 + col];
if (col < 0 && col >= -10 && g_link_l)
return pop_trob_modif(g_link_l)[row * 10 + col + 10];
if (col > 9 && col <= 19 && g_link_r)
return pop_trob_modif(g_link_r)[row * 10 + col - 10];
return 0xFF;
}
/* check_skel (seg002:1053): скелет ВСТАЁТ — его тайл стирается в пол.
* Перерисовать надо две клетки (сам тайл и соседа справа, как в оригинале:
* set_redraw_full + set_wipe на curr_tilepos и curr_tilepos+1) и на ОБЕИХ
* страницах дабл-буфера — иначе через кадр проступает старый скелет. */
void pop_skel_wake_tile(uint8_t tilepos) __banked
{
g_fg[tilepos] = TILE_FLOOR;
pop_set_redraw(tilepos, POP_RD_FLOOR, 2);
if ((tilepos % 10) < 9)
pop_set_redraw((uint8_t)(tilepos + 1), POP_RD_FLOOR, 2);
}
/* Тот же модификатор наружу (луч видимости стража, guards.c): ему нужна
* openness ворот в ЛЮБОЙ колонке луча, включая ушедшие за край комнаты. */
uint8_t pop_gate_modif(int8_t col, int8_t row) __banked
{
return gate_modif(col, row);
}
static uint8_t gate_passable(int8_t col, int8_t row)
{
uint8_t gm = gate_modif(col, row);
/* Ветки `if (gm == 0xFF) return 0;` здесь БЫТЬ НЕ ДОЛЖНО: 0xFF — это не
* только сторожевое «тайла нет» из gate_modif, но и живой модификатор
* «ворота открыты НАСОВСЕМ» (trigger_gate по button_type = щебень,
* seg007:623). Значение это появилось только когда заработал порт
* loose_land, и ворота выходили нарисованными открытыми (отрисовка
* клампит modl>188 -> 188), но непроходимыми. Сторож сюда не приходит:
* зовут нас только там, где тайл ТОЧНО ворота, а (0xFF>>2)+6 = 69 и так
* больше любого fph. */
return (uint8_t)((gm >> 2) + 6) >= (uint8_t)CD_ACT->fph;
}
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;
}
}
static uint8_t get_tile_at_char(void)
{
return get_tile(Char.curr_col, Char.curr_row);
}
static uint8_t get_tile_infrontof_char(void)
{
g_infrontx = (int8_t)(dir_front[Char.direction + 1] + Char.curr_col);
return get_tile(g_infrontx, Char.curr_row);
}
/* ---- Тайлы НАД/ЗА персонажем (для зацепа/спуска, seg006) ----------- */
static uint8_t get_tile_above_char(void)
{
return get_tile(Char.curr_col, (int8_t)(Char.curr_row - 1));
}
static uint8_t get_tile_behind_char(void)
{
return get_tile((int8_t)(dir_behind[Char.direction + 1] + Char.curr_col),
Char.curr_row);
}
static uint8_t get_tile_front_above_char(void)
{
g_infrontx = (int8_t)(dir_front[Char.direction + 1] + Char.curr_col);
return get_tile(g_infrontx, (int8_t)(Char.curr_row - 1));
}
static uint8_t get_tile_behind_above_char(void)
{
return get_tile((int8_t)(dir_behind[Char.direction + 1] + Char.curr_col),
(int8_t)(Char.curr_row - 1));
}
/* Таблицей, а не switch: тайл уже маскирован &0x1F, значит 32 байта покрывают
* весь домен, и вместо шести cp+jr выходит одно чтение по индексу. Зовётся
* на каждую колонку перебора коллизии — там это заметно. */
static const uint8_t wall_type_tbl[32] = {
0,0,0,0,1,0,0,1, /* 4,7 — стена справа (gate/doortop) */
0,0,0,0,1,2,0,0, /* 12 — то же; 13 — стена слева (mirror) */
0,0,3,0,4,0,0,0, /* 18 — chomper; 20 — стена с обеих сторон */
0,0,0,0,0,0,0,0
};
static uint8_t wall_type(uint8_t t) { return wall_type_tbl[t & 0x1F]; }
/* ---- Координаты ---------------------------------------------------- */
static int char_dx_forward(int8_t dx)
{
int d = dx;
if (Char.direction < 0) d = -d;
return (int)Char.x + d;
}
/* Деление на ширину тайла — таблицами POP_TILE_DIV/POP_TILE_MOD (резидент,
* pop_tile.c: их же читает слой фона). Вне 0..255 (у нас xpos — int, у
* DOS-оригинала байт) остаётся честная арифметика: путь редкий. */
/* col = (xpos-58)/14 округл. вниз; g_obj_xl = позиция в тайле (0..13). */
static int8_t get_tile_div_mod(int xpos)
{
if ((unsigned int)xpos < 256u) {
g_obj_xl = POP_TILE_MOD[xpos];
return POP_TILE_DIV[xpos];
} else {
int x = xpos - SCREENSPACE_X;
int xh = x / TILE_SIZEX;
int xl = x % TILE_SIZEX;
if (xl < 0) { --xh; xl += TILE_SIZEX; } /* округление вниз */
g_obj_xl = (uint8_t)xl;
return (int8_t)xh;
}
}
static int8_t get_tile_div_mod_m7(int xpos) { return get_tile_div_mod(xpos - 7); }
/* «весовая» точка кадра (dx с вычетом weight-битов флагов), в направлении. */
static int dx_weight(void)
{
int8_t offset = (int8_t)(kid_cur_dx() - (kid_cur_flags() & FRAME_WEIGHT_X));
return char_dx_forward(offset);
}
static void determine_col(void)
{
Char.curr_col = get_tile_div_mod_m7(dx_weight());
}
/* determine_col наружу — для pop_load_fram_det_col (pop_kid): порт связки
* load_frame + determine_col (seg006:0144), которую оригинал зовёт перед
* control(). См. разбор там. */
void pop_determine_col(void) __banked { determine_col(); }
/* расстояние до края тайла (для in_wall). */
static int distance_to_edge(int xpos)
{
int d;
get_tile_div_mod_m7(xpos); /* -> g_obj_xl */
d = g_obj_xl;
if (Char.direction == 0) d = TILE_RIGHTX - d; /* dir_0_right */
return d;
}
static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); }
/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- *
* Передний край персонажа = char_x_right (взгляд вправо) / char_x_left
* (влево) из set_char_collision (seg006:0723): obj_x/2 + 58, где obj_x =
* 2*char_dx_forward(cur_frame.dx) - 116 и load_frame_to_obj (seg008:1728)
* добавляет +1 для кадров «чётного пикселя» (бит 0x80 флагов XOR знак
* направления). Деление obj_x/2 в C усечено К НУЛЮ, поэтому в зоне левого
* шва (e<=57, obj_x<0) поправка +1 сдвигает край на пиксель ВПРАВО: e -> e+1.
* Это НЕ мелочь: Kid, осевший после отскока от ворот шва на x=57, имеет
* char_x_right=58 и НЕ пересекает порог leave-left (<=57) — иначе пинг-понг
* drawn_room у закрытых ворот (BUG-SEAM-PINGPONG; подтверждено потиковой
* трассой живого SDLPoP 1.23). При e>=58 формула сокращается к чистому
* char_dx_forward. ∓4 для THIN-кадров поверх. dist_from_wall_forward —
* расстояние переднего края до грани стены tiletype в колонке tcol: >0
* зазор, <=0 достал/зашёл (насколько). */
static int char_x_forward_edge(void)
{
int e = char_dx_forward(kid_cur_dx());
uint8_t fl = kid_cur_flags();
if (Char.direction < 0) fl ^= 0x80;
if (!(fl & 0x80) && e <= 57) e++; /* odd pixel + C-трунк к нулю */
return e;
}
static int char_front_coll(void)
{
int cx = char_x_forward_edge();
if (kid_cur_flags() & FRAME_THIN)
cx += (Char.direction < 0) ? 4 : -4; /* THIN: край внутрь */
return cx;
}
static int dist_from_wall_forward(uint8_t tiletype, int8_t tcol)
{
uint8_t type = wall_type(tiletype);
int coll_left, cx;
if (type == 0) return 127;
coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
cx = char_front_coll();
if (Char.direction < 0) /* лицом влево */
return cx - (coll_left + TILE_RIGHTX - wall_dr[type]);
else /* лицом вправо */
return wall_dl[type] + coll_left - cx;
}
/* in_wall (seg006:1292): персонаж оказался ВНУТРИ стены (тайл под ним =
* стена) — вытолкнуть. Аутентично SDLPoP: если он глубоко (delta>=8) ИЛИ
* перед ним тоже стена — двигаем НАЗАД к грани (6-delta), иначе ВПЕРЁД из
* стены на соседний тайл (delta+4). Вызывать ТОЛЬКО когда
* get_tile_at_char()==стена (проверяет вызывающий). */
static void in_wall(void)
{
int d = distance_to_edge_weight();
if (d >= 8 || get_tile_infrontof_char() == TILE_WALL)
d = 6 - d;
else
d += 4;
Char.x = (uint8_t)char_dx_forward((int8_t)d);
determine_col();
get_tile_at_char();
}
/* ---- Падение ------------------------------------------------------- */
static void inc_curr_row(void) { Char.curr_row++; }
/* Пики (определены ниже, перед pop_phys_tick) — нужны в land() для смерти
* при ПАДЕНИИ на выдвинутые пики (seg005 loc_5EE6). */
static int is_spike_harmful(uint8_t tilepos);
static void spiked(uint8_t tilepos);
/* fell_on_spikes (seg005 loc_5EE6): при приземлении тайл ПОД персонажем =
* пика (или пика СЗАДИ у самой кромки) и вредна → напороться (смерть).
* Возвращает 1, если умер (land прерывается). */
static uint8_t fell_on_spikes(void)
{
uint8_t tp;
if (get_tile_at_char() == TILE_SPIKE) {
tp = (uint8_t)(Char.curr_row * 10 + Char.curr_col);
} else if (distance_to_edge_weight() >= 12 &&
get_tile_behind_char() == TILE_SPIKE) {
int8_t bcol = (int8_t)(dir_behind[Char.direction + 1] + Char.curr_col);
if (bcol < 0 || bcol > 9) return 0;
tp = (uint8_t)(Char.curr_row * 10 + bcol);
} else {
return 0;
}
if (is_spike_harmful(tp)) { spiked(tp); return 1; }
return 0;
}
static void land(void)
{
uint8_t seq;
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
if (fell_on_spikes()) return; /* упал на вредные пики — смерть */
/* к краю пола — чуть назад (как оригинал) */
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3)
Char.x = (uint8_t)char_dx_forward(-3);
pop_start_chompers(Char.room, Char.curr_row); /* seg005:0148 */
/* Урон от падения (seg005:173) — ветвление по ПЕРСОНАЖУ, а не только по
* высоте. Порт построчно:
* 1 этаж (fall_y < 22): не-Кид (charid >= guard) ИЛИ меч вынут →
* меч в руку + seq_63; иначе присед seq_17. Звук/«нашумел» — только
* у Кида (у стража своё падение никого не будит).
* 2 этажа (< 33): тень падает как с одного этажа, СТРАЖ гибнет сразу,
* Кид (и скелет — баг оригинала, сохраняем) теряет 1 HP;
* 3+ этажа: гибнут все.
*
* Ветка «с мечом» на одном этаже — не косметика: seq_17_soft_land
* заканчивается ВЕЧНЫМ циклом на кадре 109, из которого выводит только
* control_crouched(), а при `sword == sword_2_drawn` управление уходит в
* control_with_sword и до него не доходит — Кид садится в присед
* НАВСЕГДА (BUG-LAND-SWORD-1). */
if (Char.fall_y < 22) {
soft_land:
if (Char.charid >= CHARID_2_GUARD || Char.sword == SWORD_2_DRAWN) {
Char.sword = SWORD_2_DRAWN;
seq = SEQ_63_ACTIVE_AFTER_FALL;
} else {
seq = SEQ_17_SOFT_LAND;
}
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
if (Char.charid == CHARID_0_KID) is_guard_notice = 1;
} else {
uint8_t deadly = (uint8_t)(Char.fall_y >= 33); /* СНЯТЬ до обнуления */
if (!deadly && Char.charid == CHARID_1_SHADOW) goto soft_land; /* seg005:189 */
if (!deadly && Char.charid == CHARID_2_GUARD) deadly = 1; /* seg005:190 */
Char.fall_x = Char.fall_y = 0;
if (deadly || pop_take_hp(1)) { /* 3+ этажа или последнее HP */
pop_take_hp(100);
pop_char_set_seq(SEQ_22_CRUSHED);
play_seq();
determine_col();
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
return;
}
if (Char.charid == CHARID_0_KID) is_guard_notice = 1; /* seg005:195 */
seq = SEQ_20_MEDIUM_LAND;
}
Char.fall_x = Char.fall_y = 0;
pop_char_set_seq(seq);
play_seq();
determine_col();
}
static void start_fall(void)
{
uint8_t frame = Char.frame, seq, tile;
/* seg006:1044 первым делом убирает меч в ножны: дальше падением рулит
* не боевой диспетчер, а обычный. Без этого Kid летит «с клинком», и
* control_with_sword разбирает кадры падения как боевые. */
Char.sword = SWORD_0_SHEATHED;
inc_curr_row();
pop_start_chompers(Char.room, Char.curr_row); /* seg006:08C1 */
/* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */
if (frame == 13) seq = SEQ_19_FALL;
else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP;
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
else if (frame >= 81 && frame < 86) {
/* сорвался, приземляясь после прыжка вверх: сдвиг ВПЕРЁД на 5 */
seq = SEQ_19_FALL;
Char.x = (uint8_t)char_dx_forward(5);
pop_load_fram_det_col();
}
else if (frame >= 150 && frame < 180) {
/* Кадры С МЕЧОМ (150..179) — ушёл в провал из боевой стойки
* (seg006:1123). Отдельная последовательность, а не seq_7: у той
* дрейф fall_x=1, и персонаж уезжал на ~тайл в сторону за полёт.
* Ветвление по персонажу — как в оригинале:
* СТРАЖ: спихнули (fall_x < 0) → seq_82, сам шагнул → seq_83;
* ПРОЧИЕ (Кид): seq_81 + взвести droppedout — по нему страж
* решает, идти ли следом вниз (guard_follows_kid_down). */
if (Char.charid == CHARID_2_GUARD) {
if (Char.fall_x < 0) {
if (Char.direction < 0 && distance_to_edge_weight() <= 7)
Char.x = (uint8_t)char_dx_forward(-5);
seq = SEQ_82_GUARD_PUSHED_OFF;
} else {
pop_droppedout = 0;
seq = SEQ_83_GUARD_FALL;
}
} else {
pop_droppedout = 1;
if (Char.direction < 0 && distance_to_edge_weight() <= 7)
Char.x = (uint8_t)char_dx_forward(-5);
seq = SEQ_81_FIGHTFALL;
}
}
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
pop_char_set_seq(seq);
play_seq();
determine_col();
/* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2).
* Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose)
* наш get_tile отдаёт WALL как СЕНТИНЕЛ кромки уровня, но в оригинале там
* КОМНАТА СНИЗУ (у дыры — пол/пусто, не стена). Без гарда in_wall() ложно
* срабатывал и откатывал X назад (6−d) → дайв падения смещался на тайл
* влево (Кид «клевал» рано над колонной вместо дыры). */
if (Char.curr_row <= 2) {
if (get_tile_at_char() == TILE_WALL) {
in_wall();
return;
}
/* Хвост seg006:1099: тайл ПЕРЕД персонажем — стена → отодвинуть на 1
* назад (иначе передний край зайдёт в грань). (seq_104 для frame 44 с
* малым зазором не портирован — в этом тесте не встречается.) */
tile = get_tile_infrontof_char();
if (tile == TILE_WALL) {
if (frame != 44 || distance_to_edge_weight() >= 6)
Char.x = (uint8_t)char_dx_forward(-1);
determine_col();
}
}
}
/* ---- Зацеп (seg006 can_grab/check_grab) --------------------------- *
* can_grab: можно ли зацепиться за g_curr_tile (curr_tile2) СКВОЗЬ through-
* тайл? Kid-only (без loose/opener-модификаторов). */
static uint8_t can_grab(uint8_t through)
{
if (through == TILE_WALL) return 0; /* сквозь стену нельзя */
if (through == TILE_DOORTOP && Char.direction >= 0) return 0; /* верх двери, лицом вправо */
if (tile_is_floor(through)) return 0; /* сквозь пол нельзя */
if (g_curr_tile == TILE_DOORTOP_FLOOR && Char.direction < 0) return 0;
if (!tile_is_floor(g_curr_tile)) return 0; /* не за что цепляться */
return 1;
}
static uint8_t can_grab_front_above(void)
{
uint8_t through = get_tile_above_char();
get_tile_front_above_char(); /* g_curr_tile = спереди-сверху */
return can_grab(through);
}
/* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за
* уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота
* подходящая. Выравнивает передний край по кромке, гасит fall_y, взводит
* grab_timer (блок climb-up на несколько кадров). */
static void check_grab(void)
{
uint8_t old_x;
if (!pop_ctrl_shift_held()) return; /* Shift не зажат */
if ((uint8_t)Char.fall_y >= 32) return; /* падает слишком быстро */
if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return;
old_x = Char.x;
Char.x = (uint8_t)char_dx_forward(-8);
determine_col();
if (!can_grab_front_above()) {
Char.x = old_x; /* не за что — назад */
determine_col();
} else {
Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
play_seq();
determine_col();
grab_timer = 12; /* (TODO: sound grab) */
}
}
static void make_loose_fall(int pos, uint8_t modifier);
/* check_grab_run_jump — ЗАЦЕП ПРЯМО В ПРЫЖКЕ, при Shift, зажатом ЗАРАНЕЕ.
*
* ВАЖНО: в оригинальной игре этого НЕТ. Это улучшение SDLPoP
* (`USE_JUMP_GRAB` + `fixes->enable_jump_grab`, seg006:1228), и там оно
* выключено, пока не включены fixes-and-enhancements. Ванильное поведение:
* зацепиться можно только в НАЧАЛЕ ПАДЕНИЯ (check_grab, кадры 102..105) —
* то есть Shift надо успеть нажать уже в полёте. Пользователь ловил ровно
* это и просил «чтобы работало и при заранее зажатом Shift», поэтому
* улучшение портировано, но за выключателем POP_ENABLE_JUMP_GRAB
* (pop_tune.h): игровая сборка сможет вернуться к оригиналу.
*
* Условия оригинала: действие — прыжок, кадр 22..23 (прыжок с места) или
* 39..41 (разбег-прыжок), «вперёд» ОТПУЩЕНО, «вверх» зажато. Дальше — та
* же проба, что у check_grab, но персонаж не падает, а сразу ставится на
* кромку и играет seq_9.
*
* Упрощение против SDLPoP: там колонка захвата правится, когда тайл
* оказался в ЧУЖОЙ комнате (curr_room != Char.room) — у нас модель
* однокомнатная, get_tile сам резолвит колонки за швом в координатах
* ТЕКУЩЕЙ комнаты, поэтому правка не нужна. Зато кнопка/плита за швом
* (grab_col вне 0..9) не сработают — тайл принадлежит соседу, а
* pop_trob_modif/g_fg у нас про свою комнату; такие случаи пропускаем. */
static uint8_t check_grab_run_jump(void)
{
uint8_t frame = Char.frame;
uint8_t grab_tile;
int8_t grab_col, grab_row;
/* Сам выключатель проверяют ТОЧКИ ВЫЗОВА (иначе SDCC ругается на
* недостижимый код при POP_ENABLE_JUMP_GRAB = 1). */
if (Char.action != ACT_RUNJUMP) return 0;
if (!((frame >= 22 && frame <= 23) || (frame >= 39 && frame <= 41))) return 0;
if (control_x != POP_CONTROL_RELEASED || control_y != POP_CONTROL_HELD_UP) return 0;
if (!can_grab_front_above()) return 0;
grab_tile = g_curr_tile; /* тайл, за который взялись */
grab_col = g_infrontx;
grab_row = (int8_t)(Char.curr_row - 1); /* СНЯТЬ до play_seq: он двигает ряд */
Char.x = (uint8_t)(pop_x_bump[grab_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
Char.x = (uint8_t)char_dx_forward(Char.direction < 0 ? -12 : 2);
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
pop_char_set_seq(SEQ_9_GRAB_WHILE_JUMPING);
play_seq();
determine_col();
grab_timer = 12;
/* Хвост оригинала: за что схватились — то и сработает. Нужен потому,
* что check_press на следующем кадре уже не поможет: кадр сменится на
* 81 (hangdrop), если Shift отпустят сразу. */
if (grab_col >= 0 && grab_col <= 9 && grab_row >= 0 && grab_row <= 2) {
uint8_t tp = (uint8_t)(grab_row * 10 + grab_col);
if (grab_tile == TILE_OPENER || grab_tile == TILE_CLOSER)
pop_trigger_button(g_room, tp, grab_tile, pop_trob_modif(g_room)[tp]);
else if (grab_tile == TILE_LOOSE) {
is_guard_notice = 1;
make_loose_fall(tp, 1);
}
}
return 1;
}
static void do_fall(void)
{
uint8_t nrow = (uint8_t)(Char.curr_row + 1);
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
check_grab(); /* ещё летит — попытка зацепа */
} else if (Char.curr_row <= 2) {
if (get_tile_at_char() == TILE_WALL)
in_wall();
if (tile_is_floor(g_curr_tile))
land();
else
inc_curr_row(); /* следующий ряд, ВКЛЮЧАЯ 2 -> 3 */
/* Ряд 3 — «за нижней кромкой комнаты» (в оригинале get_tile уходит по
* links.down и это ряд 0 комнаты снизу, seg006:005D). Нам он нужен
* ровно за тем, за чем нужен оригиналу: пока Kid летит вдоль ряда 3,
* check_grab пробует зацеп за тайл ряда 2 СВОЕЙ комнаты — иначе
* спуск «повис на кромке, отпустил, зацепился этажом ниже» через
* нижнюю границу комнаты невозможен в принципе (BUG-GRAB-SEAM-DOWN).
* Раньше ряд залипал на 2, check_grab не звался ни разу, и Kid
* пролетал мимо кромки, набирая fall_y > 32 — после чего окно
* зацепа закрыто уже по скорости. */
}
/* curr_row == 3: тайлов своей комнаты там нет (get_tile отдаёт WALL как
* сентинел), поэтому ни in_wall, ни land здесь звать нельзя — до y_land[4]
* дело всё равно не доходит: check_leave_below переводит в комнату снизу
* на y >= 211 (порт leave_room). */
}
static void fall_accel(void)
{
if (Char.action == ACT_FREEFALL) {
int fy;
/* Перо (seg006:057C). Оригинал ловит эффектом ЛЮБОГО персонажа в
* Char — зелье пьёт только Кид, но флаг глобальный, и страж, упавший
* в ту же минуту, тоже парит; SDLPoP чинит это опцией
* fix_feather_fall_affects_guards. Берём поведение С ФИКСОМ (только
* Кид): расхождение осознанное, запись в ../docs/impl_diff.md. */
if (pop_feather && Char.charid == CHARID_0_KID) {
fy = (uint8_t)Char.fall_y + FALL_ACCEL_FEATHER;
if (fy > FALL_MAX_FEATHER) fy = FALL_MAX_FEATHER;
} else {
fy = (uint8_t)Char.fall_y + FALL_ACCEL;
if (fy > FALL_MAX) fy = FALL_MAX;
}
Char.fall_y = (int8_t)fy;
}
}
static void fall_speed(void)
{
Char.y = (uint8_t)(Char.y + (uint8_t)Char.fall_y);
if (Char.action == ACT_FREEFALL) {
Char.x = (uint8_t)char_dx_forward(Char.fall_x);
determine_col();
}
}
/* ---- get_edge_distance (seg004:067C) — ТОЧНО ---------------------- *
* Расстояние переднего края до стены/края впереди + тип g_edge_type: WALL
* (стена в пределах тайла), FLOOR (пол — далеко, 11), CLOSER (край уступа
* над ямой — distance_to_edge_weight). Стена сперва под персонажем, потом
* перед ним; dist<=TILE_RIGHTX -> WALL, иначе FLOOR. */
static uint8_t g_edge_type;
static int edge_classify(int d)
{
if (d <= TILE_RIGHTX) g_edge_type = EDGE_WALL;
else { g_edge_type = EDGE_FLOOR; d = 11; }
return d;
}
int pop_edge_distance(void) __banked
{
uint8_t t; int d;
determine_col();
t = get_tile_at_char();
if (wall_type(t)) {
d = dist_from_wall_forward(t, Char.curr_col);
if (d >= 0) return edge_classify(d);
}
t = get_tile_infrontof_char();
/* Верх двери лицом ВПРАВО — кромка (в оригинале ПЕРВОЙ проверкой, до
* wall_type: у doortop wall_type != 0). */
if (t == TILE_DOORTOP && Char.direction >= 0) {
g_edge_type = EDGE_CLOSER;
return distance_to_edge_weight();
}
if (wall_type(t)) {
d = dist_from_wall_forward(t, g_infrontx);
if (d >= 0) return edge_classify(d);
}
/* ПРОВАЛИВАЮЩИЙСЯ пол впереди — КРОМКА, хотя tile_is_floor считает его
* полом. Это и есть механизм «проверки ногой»: осторожный шаг доводит
* Kid ровно до границы плиты (distance -> 0), там safe_step играет
* seq_44 (testfoot) — Kid щупает плиту ногой, та трясётся (SEQ_KNOCK_DOWN),
* и он остаётся на месте; шаг НА плиту — только со второго нажатия
* (Char.repeat). Без этой ветки плита читалась как обычный пол, и Kid
* заходил на неё первым же осторожным шагом и падал вместе с ней. */
if (t == TILE_LOOSE) {
g_edge_type = EDGE_CLOSER;
return distance_to_edge_weight();
}
/* Кнопка-закрывалка/меч/зелье: кромка, пока до границы есть зазор. */
if (t == TILE_CLOSER || t == TILE_SWORD || t == TILE_POTION) {
d = distance_to_edge_weight();
if (d) { g_edge_type = EDGE_CLOSER; return d; }
g_edge_type = EDGE_FLOOR; return 11;
}
if (tile_is_floor(t)) { g_edge_type = EDGE_FLOOR; return 11; }
g_edge_type = EDGE_CLOSER; /* яма впереди — до кромки уступа */
return distance_to_edge_weight();
}
uint8_t pop_edge_type(void) __banked { return g_edge_type; }
/* Тайл, на котором закончился разбор края (порт curr_tile2 после
* get_edge_distance). Нужен control'у: правило «у стены шагаем, а не
* бежим» в оригинале ЯВНО исключает чомпер (seg005:0577). */
uint8_t pop_edge_tile(void) __banked { return g_curr_tile; }
/* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- *
* pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в
* пустоту / seq_14 в потолок) ЛИБО прыжок-и-зацеп за уступ выше (seq_8/
* 24 — с полом сзади, seq_16 — без), и выравнивает Char.x. control_up
* гасит caller (pop_ctrl). Порт: check_jump_up/grab_up_with_floor_behind/
* jump_up_or_grab/grab_up_no_floor_behind/jump_up (без super-high-jump). */
static uint8_t grab_up_with_floor_behind(void)
{
int distance = distance_to_edge_weight();
int edge_distance = pop_edge_distance(); /* побочно ставит g_edge_type */
if (distance < 4 && edge_distance < 4 && g_edge_type != EDGE_WALL) {
Char.x = (uint8_t)char_dx_forward((int8_t)distance);
return SEQ_8_GRAB_STRAIGHT;
}
Char.x = (uint8_t)char_dx_forward((int8_t)(distance - 4));
return SEQ_24_GRAB_FORWARD;
}
static uint8_t jump_up_plain(void) /* seg005 jump_up (без super) */
{
int distance = pop_edge_distance(); /* ставит g_edge_type */
int8_t col;
uint8_t t;
if (distance < 4 && g_edge_type == EDGE_WALL)
Char.x = (uint8_t)char_dx_forward((int8_t)(distance - 3));
col = get_tile_div_mod(dx_weight() - 6);
t = get_tile(col, (int8_t)(Char.curr_row - 1));
if (t != TILE_WALL && !tile_is_floor(t))
return SEQ_28_JUMP_NOTHING_ABOVE;
return SEQ_14_JUMP_INTO_CEILING;
}
static uint8_t grab_up_no_floor_behind(void)
{
get_tile_above_char();
Char.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 10));
return SEQ_16_JUMP_UP_AND_GRAB;
}
static uint8_t jump_up_or_grab(void)
{
int distance = distance_to_edge_weight();
if (distance < 6)
return jump_up_plain();
if (!tile_is_floor(get_tile_behind_char()))
return grab_up_no_floor_behind();
Char.x = (uint8_t)char_dx_forward((int8_t)(distance - TILE_SIZEX));
determine_col(); /* load_fram_det_col */
return grab_up_with_floor_behind();
}
uint8_t pop_jump_up_seq(void) __banked
{
uint8_t through = get_tile_above_char();
get_tile_front_above_char(); /* g_curr_tile = спереди-сверху */
if (can_grab(through))
return grab_up_with_floor_behind();
through = get_tile_behind_above_char();
get_tile_above_char(); /* g_curr_tile = сверху */
if (can_grab(through))
return jump_up_or_grab();
return jump_up_plain();
}
/* run_jump (seg005:0AA8), часть «выровнять по кромке» ------------------ *
* Оригинал НЕ отталкивается откуда попало: перед разбег-прыжком он смотрит
* на 1–2 тайла вперёд и, если там провал (или пика), подгоняет X так, чтобы
* толчок пришёлся ровно на кромку пола. Иначе прыжок стартует в случайной
* фазе бегового цикла — а запаса у seq_4 почти нет: суммарный dx кадров
* 34..44 равен 62 px при ширине тайла 14, то есть ровно 4 колонки и меньше
* половины тайла сверху. Поэтому без выравнивания провал в ТРИ пустых
* тайла (перелёт с колонки 5 на колонку 1) становится непроходимым —
* BUG-RJUMP-1, уровень 2, комнаты 1 и 9.
*
* Возврат: 1 — прыгать (Char.x уже подогнан), 0 — прыжок ОТМЕНИТЬ. Отмена
* в оригинале НЕ гасит control_up: Кид бежит дальше с зажатой «вверх», и на
* следующем кадре попытка повторяется — так игрок ловит нужную фазу, просто
* удерживая клавишу. */
uint8_t pop_run_jump_align(void) __banked
{
int xpos = char_dx_forward(4);
int8_t col = get_tile_div_mod_m7(xpos);
uint8_t tf;
for (tf = 0; tf < 2; tf++) {
uint8_t t;
col = (int8_t)(col + dir_front[Char.direction + 1]);
t = get_tile(col, Char.curr_row);
if (t != TILE_SPIKE && tile_is_floor(t)) continue; /* тут пол — дальше */
{
int adj = distance_to_edge(xpos) + TILE_SIZEX * (int)tf - TILE_SIZEX;
/* Оригинал сравнивает БЕЗЗНАКОВО:
* if ((word)adj < (word)-8 || adj >= 2) { if (adj < 128) return; adj = -3; }
* что означает ровно «adj НЕ попал в [-8,-1] → прыжка нет».
* Ветка с adj = -3 недостижима: distance_to_edge ∈ [0,13] и
* tf ∈ {0,1} дают adj ∈ [-14,13], а туда нужно adj >= 128. */
if (adj < -8 || adj > -1) return 0;
Char.x = (uint8_t)char_dx_forward((int8_t)(adj + 4));
}
break;
}
return 1;
}
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
int pop_wall_ahead(void) __banked
{
int d = pop_edge_distance();
return (g_edge_type == EDGE_WALL && d <= 2);
}
/* ---- Вис (control_hanging из seg005) — тайловые решения ------------ *
* Управление держится в pop_ctrl (control_*), сюда вынесены только
* тайловые предикаты + выбор seq (+ выравнивание Char.x). */
/* grab_timer истёк — можно подтягиваться? */
uint8_t pop_hang_grab_ready(void) __banked { return grab_timer == 0; }
/* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10)
* либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал
* обратно»). Для ворот условие включает ОТКРЫТОСТЬ:
* curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6
* — под поднятой решёткой (modif >> 2 >= 6) подъём обычный. Без проверки
* модификатора Kid срывался и с открытых ворот. */
uint8_t pop_hang_climb_up_seq(void) __banked
{
uint8_t t = get_tile_above_char();
if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Char.direction == 0) ||
(t == TILE_GATE && Char.direction != 0 &&
(uint8_t)(gate_modif(Char.curr_col, (int8_t)(Char.curr_row - 1)) >> 2) < 6))
return SEQ_73_CLIMB_UP_TO_GATE;
return SEQ_10_CLIMB_UP;
}
/* Shift в висе: 1 = висит у стены/двери (caller играет seq_25); -1 =
* сорваться (hang_fall); 0 = сверху пол, остаётся висеть (ничего). */
int8_t pop_hang_wall(void) __banked
{
uint8_t t = get_tile_at_char();
if (Char.action != ACT_HANGSTRAIGHT &&
(t == TILE_WALL ||
(Char.direction < 0 && (t == TILE_DOORTOP_FLOOR || t == TILE_DOORTOP))))
return 1;
if (!tile_is_floor(get_tile_above_char()))
return -1;
return 0;
}
/* hang_fall (seg005:0A46): сорваться. Нет пола ни сзади, ни под
* персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7)
* и seq_11 (встать, конец спуска). Меняет Char.x, возвращает seq. */
uint8_t pop_hang_fall_seq(void) __banked
{
uint8_t behind = get_tile_behind_char();
uint8_t at;
if (!tile_is_floor(behind) && !tile_is_floor(get_tile_at_char()))
return SEQ_23_RELEASE_LEDGE_FALL;
at = get_tile_at_char();
if (at == TILE_WALL ||
(Char.direction < 0 && (at == TILE_DOORTOP_FLOOR || at == TILE_DOORTOP)))
Char.x = (uint8_t)char_dx_forward(-7);
return SEQ_11_RELEASE_LEDGE_LAND;
}
/* down_pressed (seg005:04C7) tile-часть: 1 = обработано (НЕ приседать —
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
* приседать. Меняет Char.x. Kid развёрнут от края (край сзади) у кромки
* пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */
/* Ветка «войти в дверь уровня» из up_pressed (seg005:0482) вместе с
* go_up_leveldoor (seg005:0574). Дверь рядом и открыта → поставить Кида в
* проём, вернуть 1 (последовательность seq_70 ставит вызывающий — это его
* слой); иначе 0 и Кид не тронут.
*
* Геометрию персонажа меняем в Kid, а НЕ в Char: окно Char вокруг
* диспетчера возвращает назад ТОЛЬКО curr_seq и sword
* (pop_savekid_state), поэтому во всём порте позицию правит pop_map — как
* в pop_down_action и pop_jump_up_seq. Запись в Char.x/Char.direction
* отсюда молча теряется (проверено: Кид уходил из проёма влево).
*
* Порядок проб как в оригинале и с той же семантикой `else if`: берём
* ПЕРВУЮ клетку, в которой стоит дверь (под персонажем → за спиной → перед
* лицом), и уже её проверяем на открытость — а не ищем «первую открытую».
*
* Открытость: `modifier >= 42`, то есть створка доехала до верха. Это
* ветка fix_exit_door, а не ванильный глобал leveldoor_open: с ванильным
* условием в дверь можно войти, пока она ещё ползёт вверх, и персонаж
* проходит сквозь опущенную створку.
*
* Стартовая комната исключена (`level.start_room != drawn_room`): в первой
* комнате уровня дверь — та, через которую вошли, и обратно в неё нельзя. */
uint8_t pop_leveldoor_enter(void) __banked
{
int8_t cols[3];
uint8_t i;
if (pop_level_start_room() == g_room) return 0;
cols[0] = Char.curr_col;
cols[1] = (int8_t)(dir_behind[Char.direction + 1] + Char.curr_col);
cols[2] = (int8_t)(dir_front[Char.direction + 1] + Char.curr_col);
for (i = 0; i < 3; i++) {
if (get_tile(cols[i], Char.curr_row) != TILE_LEVELDOOR_LEFT)
continue;
/* дверь найдена — дальше решает только её modif (см. выше) */
if (cols[i] < 0 || cols[i] > 9) return 0; /* дверь в шве: не наша */
if (pop_trob_modif(g_room)[Char.curr_row * 10 + cols[i]] < 42)
return 0;
/* go_up_leveldoor (seg005:0574): встать РОВНО в проём и развернуться
* ВЛЕВО. Направление тут не косметика: все DX последовательности
* seq_70 ОТРИЦАТЕЛЬНЫЕ (−5,−4,−4,2,3,1,3,1,2), а
* char_dx_forward при взгляде влево меняет им знак — то есть именно
* dir_FF_left и гонит персонажа ВПРАВО-ВВЕРХ по лестнице. При
* взгляде вправо тот же seq уводит его влево, ИЗ проёма. */
Char.x = (uint8_t)(pop_x_bump[cols[i] + FIRST_ONSCREEN_COLUMN] + 10);
Char.direction = -1;
return 1;
}
return 0;
}
uint8_t pop_down_action(void) __banked
{
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
Char.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */
determine_col();
return 1;
}
if (!tile_is_floor(get_tile_behind_char()) && distance_to_edge_weight() >= 8) {
uint8_t through = get_tile_behind_char();
get_tile_at_char(); /* g_curr_tile = под персонажем */
/* seg005:482: спуск с тайла-ворот лицом ВЛЕВО запрещён только пока они
* ЗАКРЫТЫ (modif >> 2 < 6); под поднятой решёткой спуск обычный. */
if (can_grab(through) &&
(Char.direction >= 0 || get_tile_at_char() != TILE_GATE ||
(uint8_t)(gate_modif(Char.curr_col, Char.curr_row) >> 2) >= 6)) {
Char.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 9));
pop_char_set_seq(SEQ_68_CLIMB_DOWN);
return 1;
}
}
return 0;
}
/* ---- Предметы: меч (seg005 check_get_item/get_item + seg006 do_pickup/
* proc_get_object) --------------------------------------------------- *
* pop_have_sword — порт глобала have_sword: «меч у Кида». Боевой режим
* (стойка с мечом, бой) НЕ реализован и здесь не нужен: сиквенс подъёма
* seq_91 сам заканчивается убиранием меча в ножны (кадры 230..240 sheathe)
* и возвратом в обычную стойку — как в оригинале до встречи со стражем. */
uint8_t pop_have_sword;
uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
* flash_color; нам хватает двух значений, которые реально встречаются. */
uint8_t pop_flash_color;
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
* тип; сам эффект применяет опкод GET_ITEM внутри сиквенса. */
static void do_pickup(int8_t obj_type, uint8_t tilepos)
{
pickup_obj_type = obj_type;
g_fg[tilepos] = TILE_FLOOR;
/* ...и В ДАННЫХ УРОВНЯ тоже, ПРЯМО СЕЙЧАС. У оригинала curr_room_tiles —
* это сама таблица уровня, так что `= tiles_1_floor` (seg006:1671) виден
* всем в тот же миг; у нас fg живёт в двух местах (буфер отрисовки g_fg
* и EMM-страница уровня), и раньше страницу правил главный цикл — ПОЗЖЕ
* в кадре, уже после pop_process_trobs.
*
* Из-за этого зазора trob поднятого зелья успевал проснуться: код тайла
* он читает из страницы уровня, там ещё зелье — и animate_potion крутил
* фазу пузырька поверх только что обнулённого модификатора,
* bubble_next_frame(0) = 1. Модификатор оставался ЕДИНИЦЕЙ навсегда
* (room_modif персистентен), а для ПОЛА в паласе modif 1 означает «узор
* не рисовать» (seg008:499, `if (num == !!level_type) return`) — синяя
* лента пропадала на месте выпитого зелья и не возвращалась даже после
* полной перерисовки комнаты (BUG-POTION-STRIPE). Теперь тайл в
* странице — пол сразу, trob того же кадра уходит в default и снимается
* (type = −1), модификатор остаётся нулём. То же лечит и меч: там
* animate_sword делал `--mod[tp]` из нуля и получал 255. */
pop_level_set_tile(g_room, tilepos, TILE_FLOOR);
/* curr_room_modif[curr_tilepos] = 0 (seg006:1676): у зелья в модификаторе
* лежал тип+фаза пузырька, у меча — счётчик блеска. Тайл стал полом —
* мусор в модификаторе не нужен (в исходное его вернёт только рестарт). */
{
uint8_t *m = pop_trob_modif(g_room);
if (m) m[tilepos] = 0;
}
pop_item_taken = (uint8_t)(tilepos + 1);
}
/* proc_get_object (seg006:16CB): эффект поднятого предмета. Зовётся из
* play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid).
*
* ПЕРВАЯ СТРОКА ОРИГИНАЛА — `if (Char.charid != charid_0_kid || ...) return`:
* предмет ЗАБИРАЕТ любой персонаж (тайл стирает do_pickup), а ЭФФЕКТ
* достаётся только Киду. На этом стоит всё спецсобытие уровня 5: тень
* выпивает зелье, оно исчезает — и не достаётся никому. Без проверки
* hitp_delta уходил бы Киду, то есть тень его лечила бы (найдено при
* портировании L5-SHADOW 2026-08-11, до живого прогона). */
void pop_proc_get_object(void) __banked
{
if (Char.charid != CHARID_0_KID) return;
if (pickup_obj_type == -1) {
pop_have_sword = 1; /* в оригинале have_sword = -1 */
/* Вспышка фона: flash_color = color_14_brightyellow, flash_time = 8
* (seg006 proc_get_object). do_flash = set_bg_attr(0, color) —
* оригинал подменяет НУЛЕВУЮ запись палитры, т.е. вспыхивает всё
* чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */
pop_flash_time = 8;
}
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_color = POP_FLASH_RED;
pop_flash_time = 2;
}
break;
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
pop_kid_add_life();
pop_flash_color = POP_FLASH_RED;
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
break;
case 3: /* ПЕРО: медленное падение */
/* feather_fall (seg000:15F8): взвести эффект + ЗЕЛЁНАЯ вспышка
* на 3 кадра (flash_color = 2). Оригинал здесь же глушит звуки
* и играет sound_39_low_weight — по нему же меряет срок; у нас
* срок задан кадрами (POP_FEATHER_FRAMES). */
pop_feather = POP_FEATHER_FRAMES;
pop_flash_color = POP_FLASH_GREEN;
pop_flash_time = 3;
break;
case 4: /* ПЕРЕВОРОТ: картинка вверх ногами */
/* toggle_upside (seg000:15E9): ВЕСЬ эффект — инверсия флага и
* пометка «перерисовать, потому что перевернулись». Ни вспышки,
* ни звука, ни урона у этой ветки нет (seg006:1885). Сам
* переворот уже нарисованного делает приложение
* (pop_flip_screen, roomtest_cold.c). */
pop_upside_want = (uint8_t)!pop_upside_want;
pop_upside_dirty = 1;
break;
case 5: /* «злое» зелье (синее): −1 HP */
/* seg006:1892. Своей вспышки у этой ветки НЕТ (в отличие от
* лечебных): красный кадр даёт общий механизм «Кид ранен»
* (flash_if_hurt, seg003:0AFC) — у нас pop_kid_hurt. Раньше мы
* ставили вспышку руками, то есть красили экран дважды.
* Спецсобытие уровня зелий: там синие склянки забирают ПОЛОВИНУ
* запаса, а не одну единицу. */
if (pop_current_level == POP_POTIONS_LEVEL)
hitp_delta = (int8_t)(-((hitp_max + 1) >> 1));
else
hitp_delta = -1;
break;
default: /* 6 «открыть все ворота» — TODO
* вместе с его уровнем (13) */
break;
}
}
pickup_obj_type = 0;
}
/* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет
* (управление идёт обычной веткой); 1 — присесть (caller зовёт crouch());
* 2 — поднять меч (caller ставит seq_91). */
uint8_t pop_get_item_action(void) __banked
{
uint8_t t;
int8_t icol;
t = get_tile_at_char();
if (t == TILE_POTION || t == TILE_SWORD) { /* стоим НА предмете */
if (!tile_is_floor(get_tile_behind_char())) return 0;
Char.x = (uint8_t)char_dx_forward(-TILE_SIZEX); /* отступить на тайл */
determine_col();
}
t = get_tile_infrontof_char(); /* заодно ставит g_infrontx */
if (t != TILE_POTION && t != TILE_SWORD) return 0;
icol = g_infrontx;
if (icol < 0 || icol > 9) return 0; /* предмет за швом — не наш */
if (Char.frame != FRAME_109_CROUCH) {
/* Ещё стоим: подровняться к предмету и присесть. */
int d = pop_edge_distance(); /* ставит g_edge_type */
if (g_edge_type != EDGE_FLOOR)
Char.x = (uint8_t)char_dx_forward((int8_t)d);
if (Char.direction >= 0)
Char.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
return 1;
}
{
uint8_t tp = (uint8_t)(Char.curr_row * 10 + icol);
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) */
}
}
/* Спецсобытие уровня 12 «появление плит» (seg006:1063, ветка внутри
* check_on_floor). После слияния с тенью (pop_united_shadow == 1) в
* верхнем ряду комнат 2 и 13 пропасти становятся проходимыми: персонаж,
* который ДОЛЖЕН БЫЛ провалиться, вместо этого достраивает под собой пол.
* Плита появляется ровно под ним, шаг за шагом — это не «включили этаж», а
* дорожка, возникающая под ногами.
*
* Возврат 1 = «пол появился, падать не надо». Перерисовываем сам тайл и
* соседа справа на ОБЕИХ страницах — тем же приёмом, что pop_skel_wake_tile
* (у оригинала это пара set_redraw_full/set_wipe на curr_tilepos и +1).
*
* Комната берётся РЕЗОЛВНУТОЙ (tile_room_of): персонаж может стоять в шве,
* и тогда пол обязан появиться в соседней комнате, как и у оригинала —
* там get_tile переставляет curr_room/curr_tilepos до присваивания. */
#define HIDDEN_FLOOR_ROOM_A 2
#define HIDDEN_FLOOR_ROOM_B 13
#define HIDDEN_FLOOR_COL_B 6 /* в комнате 13 — только правая половина */
static uint8_t floors_appear(void)
{
uint8_t room, tp;
if (pop_current_level != SHADOW_FIGHT_LEVEL) return 0;
if (pop_united_shadow >= 0) return 0; /* только ПОСЛЕ слияния */
if (Char.curr_row != 0) return 0;
if (Char.room != HIDDEN_FLOOR_ROOM_A &&
!(Char.room == HIDDEN_FLOOR_ROOM_B && Char.curr_col >= HIDDEN_FLOOR_COL_B))
return 0;
room = tile_room_of(Char.curr_col, Char.curr_row, &tp);
if (!room) return 0;
pop_level_set_tile(room, tp, TILE_FLOOR);
if (room == g_room) {
g_fg[tp] = TILE_FLOOR;
pop_set_redraw(tp, POP_RD_FLOOR, 2);
if ((tp % 10) < 9)
pop_set_redraw((uint8_t)(tp + 1), POP_RD_FLOOR, 2);
}
return 1;
}
/* ---- Диспетчер ----------------------------------------------------- */
static void check_on_floor(void)
{
if (kid_cur_flags() & FRAME_NEEDS_FLOOR) {
if (get_tile_at_char() == TILE_WALL)
in_wall(); /* забежал в стену -> выровнять */
if (!tile_is_floor(g_curr_tile) && !floors_appear())
start_fall();
}
}
static void check_action(void)
{
uint8_t action = Char.action, frame = Char.frame;
/* Зацеп прямо в прыжке (улучшение SDLPoP, seg006:0914) — ДО всего
* остального: если получилось, кадр уже сменён на вис. */
if (POP_ENABLE_JUMP_GRAB && action == ACT_RUNJUMP &&
pop_ctrl_shift_held() && check_grab_run_jump())
return;
if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) {
/* ТОЛЬКО кадр 109 (присед) — как в ванильном seg006:909. Кадры
* вставания 110..119 сюда добавляет ОПЦИОНАЛЬНЫЙ FIX_STAND_ON_THIN_AIR,
* и он состоит из ТРЁХ частей: (1) эта ветка, (2) отмена падения,
* если в 2 px позади ещё пол (seg006:1079), (3) ПРАВКА seqtbl —
* dx(1)→dx(0) и dx(4)→dx(3) в standup. У нас seqtbl извлечён из
* данных оригинала, третью часть не применить, а без неё вставание
* уносит весовую точку на 4 px вперёд — в стену. Именно половинчатый
* порт этого фикса ронял Кида сквозь пол: комната 5, приземление с
* уроном на щебень (2,7) у стены (2,8), вставание → колонка 8 = стена
* → start_fall → выпадение в комнату 6 (BUG-STANDUP-1). Ванильное
* поведение (кадр 109) от этого свободно; цена — «встать на воздухе»
* у самой кромки, ровно как в оригинале 1989 года. */
if (frame == 109)
check_on_floor();
} else if (action == ACT_FREEFALL) {
do_fall();
} else if (action == ACT_MIDAIR) {
/* Кадры 102..105 — НАЧАЛО падения (seg006:0619). Здесь fall_y ещё
* не разгоняется (fall_accel работает только в ACT_FREEFALL), то есть
* это САМОЕ ШИРОКОЕ окно зацепа: ветка стояла заглушкой, и первые
* четыре кадра падения зацеп не работал вовсе — отсюда «иногда
* цепляется, иногда нет» на входе в уровень 7. */
if (frame >= 102 && frame < 106)
check_grab();
} else if (action != ACT_HANGCLIMB) {
check_on_floor();
}
}
/* bumped_fall (seg004:0522): удар, когда под персонажем НЕ пол ИЛИ он высоко
* над полом — отскок назад и падение (seq_45). В свободном падении сиквенс
* не меняем, только гасим горизонтальную составляющую. */
static void bumped_fall(void)
{
Char.x = (uint8_t)char_dx_forward(-4);
if (Char.action == ACT_FREEFALL) {
Char.fall_x = 0;
} else {
pop_char_set_seq(SEQ_45_BUMPFALL);
play_seq();
}
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
* взводить обязаны — см. BUG-GUARD-DEAF-1. */
is_guard_notice = 1;
}
/* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ.
* Портируется целиком — раньше у нас был только хвост (seq_47), из-за чего
* терялись ДВЕ вещи: прижатие Y к полу и «жёсткий» вариант удара. Симптом
* (комната 5, прыжок в решётку): standjump бампится на кадре 25 — том самом,
* где сиквенс уже сделал dy(−6), а парного dy(+6) ещё не было, — и Kid
* оставался стоять на 6 пикселей ВЫШЕ пола, без приземления-приседания. */
static void bumped_floor(void)
{
uint8_t frame;
/* Оригинал сравнивает БЕЗЗНАКОВО: если персонаж НИЖЕ уровня пола,
* разность заворачивается в большое число и тоже даёт «падать». */
if ((uint16_t)(pop_y_land[Char.curr_row + 1] - (int16_t)Char.y) >= 15) {
bumped_fall(); /* высоко над полом — в воздухе */
return;
}
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; /* ПРИЖАТЬ к полу */
if (Char.fall_y >= 22) { /* влетел быстро — только отжать */
Char.x = (uint8_t)char_dx_forward(-5);
return;
}
Char.fall_y = 0;
/* `if (Char.alive)` (seg004:0520) — у ЖИВОГО (alive = −1) сиквенс удара
* ставится ВСЕГДА, даже если персонаж уже в отскоке. Раньше здесь стоял
* наш guard `if (Char.action == ACT_BUMPED) return;` («не рестартить») — и
* он был корнем BUG-STANDUP-1:
* 1) вставание из приседа (medland) двигает Кида на 1 px в стену —
* срабатывает бамп;
* 2) bumped_floor ПРИЖИМАЕТ y к полу (181), отменяя dy(−2), который
* medland сделал в начале;
* 3) наш guard возвращает управление, medland продолжается и делает
* свои dy(+1), dy(+1) — теперь уже ОТ уровня пола, то есть Кид
* оказывается НИЖЕ пола (y = 182);
* 4) следующий бамп видит «персонаж ниже пола» (беззнаковая разность
* заворачивается) и уводит его в bumped_fall → seq_45 → выпадение
* в комнату снизу.
* У оригинала шага (3) нет: seq_47 обрывает medland, и лишних dy не
* случается. */
if (!Char.alive) return; /* труп — только выравнивание */
frame = Char.frame;
/* Кадры прыжков и начала падения — «жёсткий» удар с приседанием
* (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */
if (frame == 24 || frame == 25 || (frame >= 40 && frame < 43) ||
(frame >= 102 && frame < 107))
pop_char_set_seq(SEQ_46_HARDBUMP);
else
pop_char_set_seq(SEQ_47_BUMP);
play_seq();
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
}
/* bump_into_opponent (seg003:08AA). Зовётся из play_kid_frame (seg000:1238)
* между set_char_collision и check_collisions, и ТОЛЬКО у Кида — у стража
* этой ветки нет (см. комментарий к guard_phys ниже).
*
* ЗАЧЕМ. Это единственная защита оригинала от «вошёл в комнату — сразу
* труп». Стража при входе никуда не отбрасывает: enter_guard (seg002:0112)
* ставит его ровно в его тайл из данных уровня, и Кид вполне может войти
* вплотную. Спасает то, что БЕЗОРУЖНЫЙ Кид, столкнувшись с вооружённым
* соперником лицом к лицу, не получает укол, а ОТСКАКИВАЕТ (seq_47).
* Остальные три страховки у нас уже есть: Кид сам достаёт меч
* (control_standing, pop_ctrl.c), страж поднимается в стойке покоя с мечом
* в ножнах (pop_guard_cold.c) и не колет ближе 12 (guards.c).
*
* Расхождений с оригиналом нет. Опциональные фиксы SDLPoP
* (fix_painless_fall_on_guard — падение на стража с высоты перестаёт быть
* безболезненным; fix_jumping_over_guard — нельзя перепрыгнуть) намеренно НЕ
* портированы: базовое поведение движка 1989 года. */
static void bump_into_opponent(void)
{
int16_t distance;
if (can_guard_see_kid < 2) return;
if (Char.sword != 0) return; /* Кид уже в боевой стойке */
if (Opp.sword == 0) return; /* у соперника меч в ножнах */
if (Opp.action >= 2) return; /* только stand / run_jump */
if (Char.direction == Opp.direction) return; /* должны быть лицом к лицу */
distance = pop_char_opp_dist();
if (distance < 0) distance = -distance;
if (distance > 15) return;
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
pop_char_set_seq(SEQ_47_BUMP);
play_seq();
}
/* ---- check_collisions (seg004:0004) -------------------------------- *
* ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка
* удара выбирается обходом 9→0 по переходу флага 0→1.
*
* ПОЧЕМУ НЕ «колонка переднего края» (как было до 2026-08-03, BUG-COLL-1):
* колонка края попадает на стену лишь пока край внутри её габаритного окна,
* а окно узкое. Для решётки в col9 (x_bump[14]=184, coll_left=191, левая
* грань 191+10=201) это ровно 201..204 — четыре пикселя. Беговой кадр
* сдвигает край больше чем на четыре, край перескакивает с ≤200 сразу на
* ≥205 (это уже колонка 10, соседняя комната, не стена) — бампа нет, а
* check_leave тут же видит char_x_right ≥ 201 и уводит Кида СКВОЗЬ
* ЗАКРЫТУЮ РЕШЁТКУ. Флаг колонки 9 в оригинале взводится при любом
* перелёте, потому что сравнивается не колонка края, а КООРДИНАТЫ:
* 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены»)
* 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань»)
* Тем же корнем лечится влёт в стену на длинном прыжке (комн. 6) и провал
* сквозь пол после приземления у стены (комн. 5).
*
* ТРИ РЯДА — как в оригинале (было: один, и это оказалось BUG-JUMPWALL-1).
* Оригинал каждый кадр считает флаги для рядов row1, row, row+1, а
* move_coll_to_prev (seg004:00DF) в начале СЛЕДУЮЩЕГО кадра выбирает из них
* тот, что соответствует ряду прошлого кадра. Так «прошлые» флаги остаются
* настоящими и на кадре смены ряда.
*
* Наш прежний упрощённый вариант (prev = 3 = «уже перекрывал») подавлял бамп
* ровно на этом кадре — а в падении ряд меняется чуть ли не каждый кадр, и
* переход 0→1 на стене приходится как раз на него. Симптом: недолетевший
* прыжок уходит СКВОЗЬ кладку по свободной траектории (ур. 3 комн. 14).
* Цена трёх рядов — 3×14 get_tile на кадр вместо 14; замерено, что это
* дешевле любого из обходных вариантов, а поведение совпадает с оригиналом.
*
* Ряды вне комнаты (−1 и 3) считаются штатно: get_tile сам резолвит ряд −1
* через комнату сверху (g_above), а ряд 3 отдаёт WALL — то же «через край
* не бампим», что и раньше. */
/* Колонки считаем от −2 до 11, а не 0..9: колонки за швом — это тайлы
* СОСЕДНЕЙ комнаты (get_tile резолвит их через g_lcol/g_rcol), и решётка
* соседа обязана бампить так же, как своя (иначе Kid уходит сквозь
* закрытые ворота шва — старый BUG-SEAM-PINGPONG). Оригинал делает то же
* самое: get_row_collision_data идёт от col(char_x_left)1 до
* col(char_x_right)+2 и резолвит комнату через find_room_of_tile, а
* координата грани (coll_tile_left_xpos) остаётся в системе ОТРИСОВАННОЙ
* комнаты — как у нас pop_x_bump[col + FIRST_ONSCREEN_COLUMN]. */
#define COLL_C0 (-2) /* первая считаемая колонка */
#define COLL_N 14 /* колонки 2..11 */
#define COLL_IDX(c) ((c) - COLL_C0)
#define COLL_ROW_NONE ((int8_t)-128)
static uint8_t coll_prev[COLL_N], coll_curr[COLL_N];
static uint8_t coll_above[COLL_N], coll_below[COLL_N];
static int8_t coll_prev_row = COLL_ROW_NONE; /* ряд, к которому относится coll_prev */
/* ОКНО перебора: какие слоты реально посчитаны. Оригинал (seg004:0047) тоже
* считает не всю комнату, а полосу вокруг персонажа — left_checked_col /
* right_checked_col; прочие слоты у него помечены «комнаты нет»
* (row_coll_room_ptr = 1) и в цикл бампа не попадают.
*
* Раньше мы считали все четырнадцать колонок, чтобы не заводить признак
* годности слота. Замер по сгенерированному асму показал, чего это стоит:
* ~940 тактов на колонку (из них 574 — get_tile), 3 ряда × 14 = ~39 600 за
* кадр у Кида плюс ~13 200 у стража (check_chomped_guard) — вместе ~12 %
* кадрового периода. Окно из 4–5 колонок режет это втрое.
*
* Признак годности у нас дешевле оригинального: не массив номеров комнат,
* который надо чистить каждый кадр, а ГРАНИЦЫ окна — четыре байта. Слот
* годен, если лежит в окне; сравнивать prev с curr можно только на
* ПЕРЕСЕЧЕНИИ двух окон. Очистки массивов нет вовсе. */
static int8_t coll_lo, coll_hi; /* окно coll_curr/above/below */
static int8_t coll_prev_lo, coll_prev_hi; /* окно coll_prev */
static int8_t win_lo, win_hi; /* результат calc_coll_window */
/* «нет колонки»: НЕ −1, потому что −1/−2 — законные колонки шва. */
#define BUMP_NONE ((int8_t)-128)
static int8_t bump_col_l; /* bump_col_left_of_wall (Kid лицом ВПРАВО) */
static int8_t bump_col_r; /* bump_col_right_of_wall (Kid лицом ВЛЕВО) */
static int coll_xl, coll_xr; /* char_x_left_coll / char_x_right_coll */
/* Ряд, к которому относилась история ДО последней инвалидации — чтобы
* pop_coll_shift мог её восстановить (см. ниже). */
static int8_t coll_last_row = COLL_ROW_NONE;
/* Смена комнаты: колонки перепроецированы, прошлые флаги недействительны.
* В оригинале это несовпадение prev_coll_room и curr_row_coll_room —
* на кадре входа бампа нет. Безопасный дефолт для ЛЮБОГО входа в комнату
* (старт уровня, респавн, чит-навигация, переход вверх/вниз); боковой
* переход тут же отменяет его через pop_coll_shift. */
static void pop_coll_invalidate(void)
{
coll_last_row = coll_prev_row;
coll_prev_row = COLL_ROW_NONE;
}
/* БОКОВОЙ переход: историю не выбрасываем, а СДВИГАЕМ (BUG-GATE-PASS-1).
*
* Оригинал (seg004:0004) индексирует флаги колонкой ВНУТРИ разрешённой
* комнаты и хранит рядом номер этой комнаты (row_coll_room_ptr), сравнивая
* prev/curr только при совпадении номеров. Решётка комнаты 8 и до перехода
* 8->6, и после лежит в слоте 9 с room = 8 — история переживает переход,
* переход флага 0->1 виден, bumped() срабатывает. У нас индекс — колонка
* ОТРИСОВАННОЙ комнаты, поэтому тот же тайл менял слот, история терялась, и
* Кид с разбега проходил сквозь закрытые ворота шва.
*
* Физически колонки сдвигаются ровно на 10 (check_leave двигает x на ∓140 =
* 10 тайлов по 14 px), и координата грани (pop_x_bump) сдвигается на те же
* 140 вместе с габаритом Кида — значит САМИ ФЛАГИ инвариантны, меняется
* только их номер слота. Отсюда весь фикс — перенумерация.
*
* Сдвигаем curr/above/below, а не prev: prev на следующем кадре всё равно
* перезапишет move_coll_to_prev, выбирая источник как раз из этих трёх.
* Освободившиеся слоты — 3 («уже перекрывал»), то есть бампа по ним нет:
* ровно то, что в оригинале даёт несовпадение номера комнаты.
*
* dcol — насколько изменились НОМЕРА колонок: уход вправо −10, влево +10.
* Отличия от дословного порта варианта A — applications/PoP/docs/impl_diff.md. */
static void coll_shift_one(uint8_t *a, int8_t dcol)
{
int8_t i, j;
if (dcol < 0) { /* источник правее — идём вперёд */
for (i = 0; i < COLL_N; i++) {
j = (int8_t)(i - dcol);
a[i] = (j < COLL_N) ? a[j] : 3;
}
} else { /* источник левее — идём назад */
for (i = COLL_N - 1; i >= 0; --i) {
j = (int8_t)(i - dcol);
a[i] = (j >= 0) ? a[j] : 3;
}
}
}
void pop_coll_shift(int8_t dcol) __banked
{
if (coll_last_row == COLL_ROW_NONE) return; /* истории и не было */
coll_prev_row = coll_last_row; /* отменить инвалидацию */
coll_shift_one(coll_curr, dcol);
coll_shift_one(coll_above, dcol);
coll_shift_one(coll_below, dcol);
/* окно посчитанных слотов едет вместе с ними; выйти за [COLL_C0..] оно
* может свободно — пересечение с окном следующего кадра само обрежет. */
coll_lo = (int8_t)(coll_lo + dcol);
coll_hi = (int8_t)(coll_hi + dcol);
}
/* set_char_collision (seg006:0723), горизонтальная часть: габарит персонажа
* в комнатных координатах. char_x_left = obj_x/2 + 58 (минус полуширина при
* взгляде вправо) — это ровно наш char_x_forward_edge для переднего края. */
static void set_char_collision(void)
{
int e = char_x_forward_edge();
int wh = ((int)CD_ACT->fpw + 1) >> 1; /* char_width_half */
if (Char.direction < 0) { coll_xl = e; coll_xr = e + wh; }
else { coll_xr = e; coll_xl = e - wh; }
if (kid_cur_flags() & FRAME_THIN) { coll_xl += 4; coll_xr -= 4; }
}
/* move_coll_to_prev (seg004:00DF): в prev кладём флаги ряда, в котором
* персонаж был НА ПРОШЛОМ кадре. Массивы curr/above/below в этот момент
* ещё хранят прошлый кадр, поэтому нужный ряд просто выбирается из них по
* разнице рядов. Условия — дословно оригинальные, включая ±3: ряд может
* «завернуться» при смене комнаты (2 -> 0 вниз, 0 -> 2 вверх), и тогда
* геометрия колонок та же, что у текущего ряда.
*
* Первый кадр (и кадр после смены комнаты, pop_coll_invalidate) прошлого
* ряда не имеет: prev = «уже перекрывал всё», бампа на нём нет. */
static void move_coll_to_prev(int8_t row)
{
const uint8_t *src;
/* Окно prev — то, по которому считались curr/above/below (у всех трёх оно
* одно). Забираем ДО того, как check_collisions пересчитает своё. */
coll_prev_lo = coll_lo; coll_prev_hi = coll_hi;
if (coll_prev_row == COLL_ROW_NONE) {
memset(coll_prev, 3, COLL_N);
coll_prev_lo = COLL_C0; coll_prev_hi = COLL_C0 + COLL_N - 1;
return;
}
if (row == coll_prev_row || row + 3 == coll_prev_row || row - 3 == coll_prev_row)
src = coll_curr; /* ряд не менялся */
else if (row + 1 == coll_prev_row || row - 2 == coll_prev_row)
src = coll_above; /* поднялся на ряд */
else
src = coll_below; /* опустился на ряд */
/* memcpy, а не цикл: SDCC пересчитывал адрес назначения на КАЖДОЙ
* итерации через слот кадра IX — 14 байт обходились в 5 514 тактов. */
memcpy(coll_prev, src, COLL_N);
}
/* Окно перебора (seg004:0047). Считать ПОСЛЕ set_char_collision — берётся из
* coll_xl/coll_xr. Клампы: справа как в оригинале (не дальше последней
* колонки), слева — по нашему COLL_C0 (оригиналу клампить нечего, у него
* индекс приходит уже разрешённым внутрь комнаты). */
static void calc_coll_window(void)
{
int8_t hi = (int8_t)(get_tile_div_mod_m7(coll_xr) + 2);
int8_t lo = (int8_t)(get_tile_div_mod_m7(coll_xl) - 1);
if (hi > COLL_C0 + COLL_N - 1) hi = COLL_C0 + COLL_N - 1;
if (lo < COLL_C0) lo = COLL_C0;
if (hi < lo) hi = lo;
win_lo = lo; win_hi = hi;
}
/* Тело перебора: src — тайлы ряда ПОДРЯД, dst — куда класть флаги. Всё
* остальное вынесено в статики намеренно: у SDCC z80 каждый лишний
* параметр/локал уезжает в кадр IX, а `ld -n(ix)` стоит 19 тактов номинала
* (у нас с wait-state'ами ~46).
*
* Замер одной итерации в MAME: пустая колонка 750 тактов, колонка-стена
* ~1 700 (там две 16-битные знаковые сверки граней). */
static uint8_t scan_n; /* сколько колонок осталось */
static int scan_left; /* левая грань текущей колонки */
static void coll_scan(const uint8_t *src, uint8_t *dst)
{
do {
uint8_t wt = wall_type_tbl[*src++ & 0x1F];
uint8_t f = 0;
if (wt) {
if (wall_dl[wt] + scan_left < coll_xr) f = 1;
if (scan_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2;
}
*dst++ = f;
scan_left += TILE_SIZEX;
} while (--scan_n);
}
/* Всё, что НЕ зависит от ряда, считается один раз на кадр. Раньше это жило
* в теле ряда, и пролог одного ряда стоил тысячи тактов при том, что сам
* цикл по четырём-пяти колонкам — около трёх. */
static uint8_t scan_n0;
static int scan_left0;
static uint8_t scan_off; /* индекс первого слота в flags[] */
static void coll_scan_prepare(void)
{
scan_left0 = pop_x_bump[win_lo + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
scan_n0 = (uint8_t)(win_hi - win_lo + 1);
scan_off = (uint8_t)COLL_IDX(win_lo);
}
/* Базы ряда: указатели, которые индексируются НОМЕРОМ КОЛОНКИ (в том числе
* отрицательным). Ноль = «этой стороны нет» → стена. Ряд разрешается один
* раз на ряд, а не в каждом get_tile: тот на каждый вызов заново решал «ряд
* −1? за комнатой? слева/справа?» и считал row*10+col сдвигами (1 422 такта
* на вызов при 12-15 вызовах за кадр). */
static const uint8_t *rb_own, *rb_lft, *rb_rgt;
/* Ряд, целиком состоящий из стены — для стороны, которой нет (край уровня,
* ряд вне комнаты). Дешевле, чем ветка «а тут стена» внутри цикла. */
static const uint8_t wall_row[COLL_N] = {
TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,
TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL
};
/* get_row_collision_data (seg004:0185) для одного ряда: бит0 = «персонаж
* достал левую грань стены колонки», бит1 = «правую». Считаем ОКНО
* win_lo..win_hi (см. calc_coll_window), слоты вне окна не трогаем — их
* годность определяют границы окна, а не содержимое.
*
* Окно режется на НЕПРЕРЫВНЫЕ пробеги: комната слева (колонки < 0), своя
* (0..9), справа (> 9). По каждому идёт тот же coll_scan с шагающим
* указателем, поэтому вопрос «а эта колонка чья?» решается по одному разу
* на пробег, а не на колонку. Первая версия собирала тайлы окна во
* временный буфер тернарником на колонку — 1 340 тактов на колонку, вдвое
* дороже самого перебора; у Кида, стоящего в колонке 0, окно начинается с
* −1, так что это был не редкий, а ОБЫЧНЫЙ путь.
*
* scan_left между пробегами не сбрасывается: coll_scan сам двигает его на
* тайл за колонку — ровно как оригинал (coll_tile_left_xpos += TILE_SIZEX). */
static void coll_row(uint8_t *flags)
{
int8_t lo = win_lo, last;
uint8_t n;
uint8_t *dst = flags + scan_off;
scan_left = scan_left0; /* coll_scan сам двигает его дальше */
if (lo < 0) { /* комната СЛЕВА */
last = win_hi < -1 ? win_hi : -1;
n = (uint8_t)(last - lo + 1);
scan_n = n; /* coll_scan обнулит */
coll_scan(rb_lft ? rb_lft + lo : wall_row, dst);
dst += n;
lo = (int8_t)(last + 1);
}
if (lo <= win_hi && lo <= 9) { /* СВОЯ комната */
last = win_hi < 9 ? win_hi : 9;
n = (uint8_t)(last - lo + 1);
scan_n = n;
coll_scan(rb_own ? rb_own + lo : wall_row, dst);
dst += n;
lo = (int8_t)(last + 1);
}
if (lo <= win_hi) { /* комната СПРАВА */
scan_n = (uint8_t)(win_hi - lo + 1);
coll_scan(rb_rgt ? rb_rgt + lo : wall_row, dst);
}
}
/* Разрешить ряд «с нуля» — для одиночного вызова (страж, check_chomped_guard).
* В check_collisions три ряда получаются сдвигом баз на ±10 без пересчёта. */
static void coll_row_bases(int8_t row)
{
rb_own = rb_lft = rb_rgt = 0;
if (row >= 0 && row <= 2) {
int ro = (int)row * 10;
rb_own = g_fg + ro;
if (g_link_l) rb_lft = g_lcol + ro + 10;
if (g_link_r) rb_rgt = g_rcol + ro - 10;
} else if (row == -1 && g_above && g_link_u) {
rb_own = g_above; /* ряд 2 комнаты сверху */
}
}
/* Одиночный ряд во ВНЕШНИЙ массив (единственный вызывающий —
* check_chomped_guard).
*
* coll_scan_prepare() тут ОБЯЗАТЕЛЕН, и это не перестраховка: coll_row
* пишет по `flags + scan_off`, а число байт берёт из win_lo/win_hi. Без
* prepare scan_off/scan_left0 остаются от ПРОШЛОГО окна (его считает
* check_collisions для Кида), а окно тут уже своё, стражево. Когда
* стражево окно шире или правее кидова, запись уезжает за границу
* flags[COLL_N] — а сразу за ней в кадре check_chomped_guard лежит
* СОХРАНЁННЫЙ IX. Затёртый младший байт превращал кадровый указатель
* main (0xBFFA) в 0xBF00, и дальше main читал свои локали из чужого
* стекового мусора: игра «зависала», потому что отладочный `frozen`
* становился ненулевым (BUG-GUARD-IX-1). Заодно чинится и сам расчёт:
* scan_left0 задаёт x колонок, и страж считался по координатам Кида. */
static void get_row_collision_data(int8_t row, uint8_t *flags)
{
coll_scan_prepare();
coll_row_bases(row);
coll_row(flags);
}
static void check_collisions(void)
{
int8_t col;
bump_col_l = bump_col_r = BUMP_NONE;
/* seg004:0044 — на кадрах разворота коллизии не считаются вовсе. */
if (Char.action == ACT_TURN) return;
/* Вне рядов комнаты (падение мимо пола) get_tile отдаёт WALL во всех
* колонках — считать по нему нечего. Стену в падении держит
* do_fall/in_wall, выход из комнаты — check_leave_below. */
if (Char.curr_row < 0 || Char.curr_row > 2) {
/* И coll_last_row тоже: curr/above/below остались от старого кадра,
* сдвигать их при боковом переходе нечего (поведение как раньше). */
coll_prev_row = coll_last_row = COLL_ROW_NONE;
return;
}
set_char_collision();
move_coll_to_prev(Char.curr_row); /* заодно снимет окно prev */
coll_prev_row = Char.curr_row;
calc_coll_window();
coll_lo = win_lo; coll_hi = win_hi;
coll_scan_prepare();
/* Порядок важен: prev уже забран, теперь три ряда пересчитываются
* НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). Ряд здесь
* гарантированно 0..2, поэтому соседние ряды — это те же базы ±10, без
* повторного разрешения. */
{
int ro = (int)Char.curr_row * 10;
const uint8_t *own = g_fg + ro;
const uint8_t *lft = g_link_l ? g_lcol + ro + 10 : 0;
const uint8_t *rgt = g_link_r ? g_rcol + ro - 10 : 0;
rb_own = own; rb_lft = lft; rb_rgt = rgt;
coll_row(coll_curr);
if (Char.curr_row < 2) { /* ряд ниже */
rb_own = own + 10;
rb_lft = lft ? lft + 10 : 0;
rb_rgt = rgt ? rgt + 10 : 0;
} else {
rb_own = rb_lft = rb_rgt = 0; /* ряд 3 — стена везде */
}
coll_row(coll_below);
if (Char.curr_row > 0) { /* ряд выше */
rb_own = own - 10;
rb_lft = lft ? lft - 10 : 0;
rb_rgt = rgt ? rgt - 10 : 0;
} else if (g_above && g_link_u) { /* ряд −1 = ряд 2 сверху */
rb_own = g_above; rb_lft = rb_rgt = 0;
} else {
rb_own = rb_lft = rb_rgt = 0;
}
coll_row(coll_above);
}
/* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
* в которой флаг перешёл 0→1. Только по ПЕРЕСЕЧЕНИЮ окон: вне его
* сравнивать нечего (у оригинала там prev_coll_room != curr_row_coll_room
* или curr_row_coll_room < 0 — тот же смысл). */
{
int8_t lo = coll_lo > coll_prev_lo ? coll_lo : coll_prev_lo;
int8_t hi = coll_hi < coll_prev_hi ? coll_hi : coll_prev_hi;
for (col = hi; col >= lo; --col) {
uint8_t p = coll_prev[COLL_IDX(col)], c = coll_curr[COLL_IDX(col)];
if (!(p & 1) && (c & 1)) bump_col_l = col;
if (!(p & 2) && (c & 2)) bump_col_r = col;
}
}
}
/* is_obstacle (seg004:037E): препятствие ли тайл колонки на самом деле.
* Побочно оставляет тайл в g_curr_tile (как curr_tile2 у оригинала). */
static uint8_t is_obstacle_at_col(int8_t col)
{
uint8_t t = get_tile(col, Char.curr_row);
if (t == TILE_POTION) return 0; /* зелье не бампит */
if (t == TILE_GATE) return (uint8_t)!gate_passable(col, Char.curr_row);
if (t == TILE_CHOMPER) /* закрыт только при modif==2 */
return (uint8_t)(col >= 0 && col <= 9 &&
pop_trob_modif(g_room)[Char.curr_row * 10 + col] == 2);
/* Прыжок СКВОЗЬ ЗЕРКАЛО (seg004:0239). Зеркало вообще-то «стена слева»
* (wall_type = 2), но для Кида в БЕГОВОМ ПРЫЖКЕ справа налево оно
* перестаёт быть препятствием: он пролетает насквозь, зеркало «бьётся»
* (modif 0x56) и взводится флаг рождения тени. Условия — все четыре, как
* в оригинале: только Кид, только кадры бегового прыжка 39..43, только
* направление влево. */
if (t == TILE_MIRROR && Char.charid == CHARID_0_KID &&
Char.frame >= 39 && Char.frame < 44 && Char.direction < 0 &&
col >= 0 && col <= 9) {
pop_trob_modif(g_room)[Char.curr_row * 10 + col] = 0x56;
pop_jumped_mirror = -1;
return 0;
}
return 1;
}
/* bumped (seg004:0448). delta_x — сдвиг персонажа В КООРДИНАТАХ (не
* «вперёд»), push_dir — куда толкает: <0 влево, >=0 вправо. Чем кончится
* удар, решает тайл, на котором персонаж оказался после отжатия: сквозь
* стену/верх двери он стоит на СОСЕДНЕЙ клетке (в сторону толчка), для
* прочих препятствий (ворота, зеркало, кусаки) — на клетке препятствия. */
static void bumped(int delta_x, int8_t push_dir, int8_t tcol)
{
uint8_t ct;
if (Char.alive >= 0) return; /* труп не бампит */
Char.x = (uint8_t)((int)Char.x + delta_x);
determine_col();
ct = g_curr_tile; /* тайл препятствия */
if (push_dir < 0) { /* толчок ВЛЕВО */
if (ct == TILE_WALL)
ct = get_tile((int8_t)(tcol - 1), Char.curr_row);
} else { /* толчок ВПРАВО */
if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL)
ct = get_tile((int8_t)(tcol + 1), Char.curr_row);
}
if (tile_is_floor(ct)) bumped_floor();
else bumped_fall();
determine_col();
}
/* check_bumped_look_right (seg004:030A): Kid смотрит ВПРАВО и достал ЛЕВУЮ
* грань стены — отжать влево. */
static void check_bumped_look_right(void)
{
int8_t col = bump_col_l;
uint8_t wt;
int coll_left;
if (Char.direction != 0) return; /* (меча в руке у нас нет) */
if (!is_obstacle_at_col(col)) return;
/* Прежде чем удариться о стену — попытка зацепиться за её верх
* (seg004:0205, то же улучшение). Не вышло — тайл препятствия надо
* перечитать: проба зацепа сбила g_curr_tile. */
if (POP_ENABLE_JUMP_GRAB && pop_ctrl_shift_held()) {
if (check_grab_run_jump()) return;
if (!is_obstacle_at_col(col)) return;
}
wt = wall_type(g_curr_tile);
if (!wt) return;
coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
bumped(wall_dl[wt] + coll_left - coll_xr, -1, col);
}
/* check_bumped_look_left (seg004:02D2): Kid смотрит ВЛЕВО и достал ПРАВУЮ
* грань стены — отжать вправо. */
static void check_bumped_look_left(void)
{
int8_t col = bump_col_r;
uint8_t wt;
int coll_left;
if (Char.direction >= 0) return;
if (!is_obstacle_at_col(col)) return;
if (POP_ENABLE_JUMP_GRAB && pop_ctrl_shift_held()) { /* seg004:0186 */
if (check_grab_run_jump()) return;
if (!is_obstacle_at_col(col)) return;
}
wt = wall_type(g_curr_tile);
if (!wt) return;
coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
bumped(coll_left - wall_dr[wt] + TILE_RIGHTX - coll_xl, 0, col);
}
/* dist_from_wall_behind (seg004:0607) — зеркало dist_from_wall_forward: та
* же арифметика, но от ЗАДНЕЙ грани габарита (лицом вправо задняя грань —
* левая, и наоборот). Нужна только check_guard_bumped. */
static int dist_from_wall_behind(uint8_t tiletype, int8_t tcol)
{
uint8_t type = wall_type(tiletype);
int coll_left;
if (type == 0) return 99;
coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
if (Char.direction >= 0) /* лицом вправо */
return coll_xl - (coll_left + TILE_RIGHTX - wall_dr[type]);
return wall_dl[type] + coll_left - coll_xr; /* лицом влево */
}
/* check_guard_bumped (seg004:0522) — стражья замена check_bumped, и она
* НЕ про «упереться на бегу»: срабатывает, только когда персонажа с
* ВЫНУТЫМ МЕЧОМ теснят спиной в стену (его собственный шаг назад в бою или
* наступающий Кид). Отсюда и проверки: action = run_jump, жив, меч в руке,
* а расстояние меряется от ЗАДНЕЙ грани.
*
* Порядок проб как в оригинале, включая побочный эффект: если тайл под
* персонажем не подошёл и он смотрит вправо, оригинал перечитывает тайл
* СЛЕВА (`--tile_col`), и дальше is_obstacle/dist_from_wall_behind считают
* уже по нему. */
static void check_guard_bumped(void)
{
int8_t col = Char.curr_col, row = Char.curr_row;
uint8_t t, hit;
int delta_x;
if (Char.action != ACT_RUNJUMP || Char.alive >= 0 ||
Char.sword < SWORD_2_DRAWN) return;
t = get_tile_at_char();
hit = (uint8_t)(t == TILE_WALL || t == TILE_DOORTOP_FLOOR ||
(t == TILE_GATE && !gate_passable(col, row)));
if (!hit && Char.direction >= 0) {
col--;
t = get_tile(col, row);
hit = (uint8_t)(t == TILE_DOORTOP_FLOOR ||
(t == TILE_GATE && !gate_passable(col, row)));
}
if (!hit) return;
set_char_collision();
if (!is_obstacle_at_col(col)) return;
delta_x = dist_from_wall_behind(g_curr_tile, col);
if (delta_x < 0 && delta_x > -13) {
Char.x = (uint8_t)char_dx_forward(-delta_x);
pop_char_set_seq(SEQ_65_BUMP_FWD_SWORD);
play_seq();
pop_load_fram_det_col();
}
}
/* check_bumped (seg004:029D). */
static void check_bumped(void)
{
/* Гарды РОВНО как в оригинале: вис и подтягивание. ПАДЕНИЕ И ПОЛЁТ НЕ
* ИСКЛЮЧЕНЫ — bumped_fall (seg004:04E4) специально разбирает случай
* actions_4_in_freefall (гасит fall_x и отжимает на 4). Наш прежний
* guard `action == FREEFALL || MIDAIR -> return` и был вторым корнем
* BUG-COLL-1: длинный прыжок в стену (комната 6, между (0,6) и (0,7))
* ничем не останавливался, и Кид оказывался ВНУТРИ кладки. */
if (Char.action == ACT_HANGCLIMB || Char.action == ACT_HANGSTRAIGHT) return;
if (Char.frame >= 135 && Char.frame < 149) return; /* кадры подъёма/спуска */
/* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb),
* т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку
* hang->hang_fall->seq_11 (правильное приземление). */
if (Char.frame >= 87 && Char.frame < 100) return;
if (bump_col_l != BUMP_NONE) check_bumped_look_right();
else if (bump_col_r != BUMP_NONE) check_bumped_look_left();
}
/* ---- Loose floors (seg007) ---------------------------------------- *
* Состояние на тайл: 0 = покой; 0x80..0x83 = тряска (do_knock); 1..11 =
* отсчёт до падения (на тайле стоят/пробили). Порт animate_loose/
* make_loose_fall/check_press. Перерисовка — в pop_bg (shake/bake). */
uint8_t pop_loose_modif[30]; /* публично: читает pop_bg при отрисовке */
/* Гейт холостого хода pop_loose_tick. В подавляющем большинстве кадров
* (и в целых комнатах) НИ ОДНА плита не анимируется, а два цикла по 30 и 10
* позициям всё равно отрабатывают: замер 11/15 — 9 852 такта на комнату,
* где loose-плит нет вовсе.
*
* Ставится ПРИ ВЗВОДЕ фазы — все пять мест записи ненулевого значения лежат
* в этом файле (make_loose_fall, ветка потолка в check_press, do_knock для
* обоих рядов, восстановление из room_modif в pop_loose_reset). Снимает его
* САМ цикл, когда прошёл оба массива и не встретил ни одной ненулевой фазы.
*
* Асимметрия намеренная: ложная единица стоит одного холостого прохода,
* ложный ноль — застывшей навсегда плиты. Поэтому взвод обязан стоять
* рядом с КАЖДОЙ записью, а снятие — только по факту пустого прохода. */
static uint8_t loose_any;
/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */
/* make_loose_fall (seg007:0EF6): взвести отсчёт падения, если ещё не идёт. */
static void make_loose_fall(int pos, uint8_t modifier)
{
if (pos < 0 || pos >= 30) return;
if ((g_fg[pos] & 0x1F) != TILE_LOOSE) return;
if ((g_fg[pos] & 0x20)) return; /* «solid» loose — не от шага */
if ((int8_t)pop_loose_modif[pos] <= 0) { /* покой/тряска, не отсчёт */
pop_loose_modif[pos] = modifier;
loose_any = 1;
}
}
/* died_on_button (seg007:0776): на кнопке КТО-ТО УМЕР — эффект становится
* ПОСТОЯННЫМ, а сама кнопка с тайла снимается.
*
* Открывалка превращается в обычный пол с нулевым модификатором, а связь
* дёргается типом «щебень» (TILE_DEBRIS) — у trigger_gate это отдельная
* ветка «открыть насовсем»: недооткрытые ворота получают trob-тип 2, уже
* открытые — modif 0xFF (открыты навсегда). Любая другая кнопка становится
* заклиненной (TILE_STUCK): нажатой она и останется.
*
* ЗАЧЕМ ЭТО ВООБЩЕ НУЖНО. Труп не «держит» кнопку — он её ЛОМАЕТ. Разница
* видна сразу: держал бы — ворота закрылись бы, как только тело убрали или
* уровень перезапустили. У Кида перезапуск всё равно перечитывает тайлы, а
* вот когда на кнопке гибнет СТРАЖ, перезапуска нет и кнопка заклинена до
* конца уровня.
*
* Тайл пишем и в страницу уровня (переживёт выход из комнаты, как do_pickup),
* и в живую g_fg отрисованной комнаты — вместе с пометкой на перерисовку
* своей ячейки и правой (у кнопки и пола разные правые грани). */
static void died_on_button(uint8_t room, uint8_t tp, uint8_t code)
{
uint8_t *mod = pop_trob_modif(room);
uint8_t modifier = mod[tp]; /* индекс связи — ДО затирания */
uint8_t button_type = code;
uint8_t newtile;
if (code == TILE_OPENER) {
newtile = TILE_FLOOR;
mod[tp] = 0;
button_type = TILE_DEBRIS; /* trigger_gate: «открыть насовсем» */
} else {
newtile = TILE_STUCK;
}
pop_level_set_tile(room, tp, newtile);
if (room == g_room) {
g_fg[tp] = newtile;
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
if (tp % 10 < 9)
pop_set_redraw((uint8_t)(tp + 1), POP_RD_FLOOR, 2);
}
pop_trigger_button(room, tp, button_type, modifier);
}
/* check_press (seg006:0EC8, упрощ.): Kid стоит на loose → make_loose_fall;
* frame 79 (прыжок вверх) с loose СВЕРХУ → пробить сверху. */
static void check_press(void)
{
uint8_t frame = Char.frame, action = Char.action;
uint8_t above_press = 0; /* «нажимается» тайл СВЕРХУ */
/* Кадры виса (87..99) и начала подъёма (135..140): нажимается тайл, за
* КОТОРЫЙ ПЕРСОНАЖ ДЕРЖИТСЯ, т.е. верхний (seg006:1688). Отсюда падение
* плиты, за край которой Kid зацепился, — а не только той, по которой он
* ударил снизу прыжком (кадр 79). */
if ((frame >= 87 && frame < 100) || (frame >= 135 && frame < 141)) {
above_press = 1;
} else if (action == ACT_BUMPED || action == ACT_TURN || action < ACT_HANGCLIMB) {
if (frame == 79) above_press = 1; /* прыжок вверх — удар снизу */
} else {
return; /* seg006:1704 — прочее не жмёт */
}
if (above_press) {
if (Char.curr_row == 0) {
/* Ряд 0: «сверху» — ряд 2 комнаты СВЕРХУ (в оригинале get_tile с
* row=1 уходит по links.up, find_room_of_tile seg006:005D).
* Плита-ПОТОЛОК; кнопок на потолке в этом тесте нет. */
int8_t c = Char.curr_col;
if (g_above && g_link_u && c >= 0 && c <= 9 &&
(g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) &&
(int8_t)pop_ceil_modif[c] <= 0) {
pop_ceil_modif[c] = 1; /* make_loose_fall(1) */
loose_any = 1;
is_guard_notice = 1; /* seg006:1734 */
}
} else if (get_tile_above_char() == TILE_LOOSE) {
make_loose_fall((Char.curr_row - 1) * 10 + Char.curr_col, 1);
is_guard_notice = 1; /* seg006:1734 */
}
return;
}
/* Стояние: кадр обязан требовать пол; нажимается тайл ПОД персонажем.
* ACT_TURN обязателен (seg006:1691): при развороте на кнопке иначе
* trigger_button не зовётся кадр-два → таймер связи падает <2 → кнопка
* ложно отжимается и жмётся снова. */
if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return;
{
uint8_t t = get_tile_at_char();
uint8_t tp = 0;
/* Кнопка может лежать в СОСЕДНЕЙ комнате — Кид стоит в шве
* (curr_col = 1 или 10). Комнату и tilepos берём резолвнутые,
* иначе индекс связи читается из чужого тайла (см. tile_room_of). */
uint8_t btn_room = tile_room_of(Char.curr_col, Char.curr_row, &tp);
if ((t == TILE_OPENER || t == TILE_CLOSER) && btn_room) {
/* seg006:1707 — ЖИВОЙ жмёт кнопку на 5 кадров, МЁРТВЫЙ ломает её
* насовсем. `alive < 0` = жив (соглашение оригинала). */
if (Char.alive < 0)
pop_trigger_button(btn_room, tp, t, pop_trob_modif(btn_room)[tp]);
else
died_on_button(btn_room, tp, t);
}
else if (t == TILE_LOOSE) {
/* loose живёт в СВОЕЙ комнате (make_loose_fall работает с g_fg),
* поэтому здесь по-прежнему координаты персонажа. */
make_loose_fall((uint8_t)(Char.curr_row * 10 + Char.curr_col), 1);
is_guard_notice = 1; /* seg006:1734 */
}
}
}
/* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от
* приземления/удара трясёт ВСЕ loose-полы в ряду tile_row (не роняя их —
* modif 0x80..0x83 = чистая тряска, сбрасывается в pop_loose_tick). Трясём
* только из ПОКОЯ (modif==0): если тайл уже дрожит/падает — не перебиваем. */
static void do_knock(int tile_row)
{
int col, pos;
/* СПЕЦКЕЙС УРОВНЯ 13 (loose_make_shake, seg007:949): сотрясение НЕ
* трясёт плиты вовсе. Причина та же, что у первого спецкейса: тряска
* ставит фазу 0x80, а на этом уровне у плит уже лежит СВОЙ отложенный
* старт (0xF0..0xFF), и приземление Кида сбросило бы его на 0x80 —
* гряда посыпалась бы разом вместо вразнобой. */
if (pop_current_level == POP_LOOSE_TILES_LEVEL) return;
if (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */
if (!g_above || !g_link_u) return;
for (col = 0; col < 10; col++)
if ((g_above[col] & 0x1F) == TILE_LOOSE && pop_ceil_modif[col] == 0) {
pop_ceil_modif[col] = 0x80;
loose_any = 1;
}
return;
}
if (tile_row < 0 || tile_row > 2) return;
for (col = 0; col < 10; col++) {
pos = tile_row * 10 + col;
if ((g_fg[pos] & 0x1F) != TILE_LOOSE) continue;
if (pop_loose_modif[pos] == 0) {
pop_loose_modif[pos] = 0x80;
loose_any = 1;
}
}
}
/* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl-
* командах KNOCK_UP/DOWN) тряхнуть ряд. KNOCK_DOWN (knock=-1, приземление) →
* ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */
void pop_check_knock(void) __banked
{
if (knock) {
do_knock(Char.curr_row - (knock > 0));
knock = 0;
}
}
/* Сдать НЕЗАКОНЧЕННЫЕ loose уходящей комнаты в персистентное хранилище —
* звать ПЕРЕД сменой комнаты, пока g_room/g_fg/g_link_u ещё старые.
*
* pop_loose_modif[30] — фаза тайлов ТЕКУЩЕЙ комнаты, и раньше смена комнаты
* её просто теряла: плита, по которой Кид пробежал и ушёл вправо, оставалась
* целой (уровень 10, комната 2, тайл 2,9). В оригинале терять нечего —
* там loose это trob в общем списке, а фаза лежит в curr_room_modif, то есть
* в самой таблице уровня. Порт того же: фаза уезжает в room_modif (он
* персистентен per-room), а досчитывает её animate_loose в pop_trob.c.
*
* Пишем фазу ВСЕМ loose-тайлам, включая нулевые: room_modif — источник
* истины, а pop_loose_modif лишь его копия на время пребывания в комнате,
* и оставить там прошлое значение значило бы «оживить» уже отдрожавшую
* плиту при следующем входе. trob заводим только на незаконченные. */
void pop_loose_leave_room(void) __banked
{
uint8_t *mod;
int i;
if (!g_fg || !g_room) return;
mod = pop_trob_modif(g_room);
if (mod) {
for (i = 0; i < 30; i++) {
if ((g_fg[i] & 0x1F) != TILE_LOOSE) continue;
mod[i] = pop_loose_modif[i];
if (pop_loose_modif[i]) pop_add_trob(g_room, (uint8_t)i, 0);
}
}
/* Плиты-ПОТОЛКИ — ряд 2 комнаты сверху, tilepos 20+col. */
if (g_above && g_link_u) {
mod = pop_trob_modif(g_link_u);
if (mod) {
for (i = 0; i < 10; i++) {
if ((g_above[i] & 0x1F) != TILE_LOOSE) continue;
mod[20 + i] = pop_ceil_modif[i];
if (pop_ceil_modif[i])
pop_add_trob(g_link_u, (uint8_t)(20 + i), 0);
}
}
}
}
/* Решётка ворот должна идти ПОВЕРХ персонажа? Порт первого условия
* draw_tile_fore (seg008:0D15):
*
* if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
* Kid.curr_col == drawn_col - 1 && Kid.room != room_R)
* draw_gate_fore();
*
* То есть: рисуя тайл (row, col), оригинал кладёт бары ворот в FOREtable —
* поверх всего, что уже нарисовано, — если тайл СЛЕВА это ворота и персонаж
* стоит ИМЕННО НА НИХ. Графика ворот принадлежит правому краю своего тайла
* и рисуется в проходе СЛЕДУЮЩЕЙ колонки, отсюда сдвиг на единицу.
*
* Живёт в карте, а не в слое фона, по двум причинам: спрашивает она про
* ТАЙЛ и позицию персонажа (это знание карты), и её линкует хост-харнесс —
* решение проверяется тестом без эмулятора (t_char: gate_fore_over_char).
* Слою фона остаётся только рисование (draw_gate_fore в pop_bg.c).
*
* РАСХОЖДЕНИЕ С ОРИГИНАЛОМ (осознанное, см. docs/impl_diff.md): оригинал
* жёстко спрашивает про Kid, потому что foretable у него ОДНА на проход
* тайлов; у нас fore-проход идёт по персонажу, поэтому спрашиваем про того,
* кого рисуем. Наш вариант — надмножество: страж, стоящий в проёме ворот,
* у нас тоже уходит ЗА решётку, а в оригинале остался бы перед ней. */
uint8_t pop_gate_over_char(const pop_char_t *ch) __banked
{
if (!ch || ch->room != g_room) return 0;
if (ch->curr_row < 0 || ch->curr_row > 2) return 0;
/* Колонка 9: бары легли бы в колонку 10, то есть в СОСЕДНЮЮ комнату —
* у оригинала это отсекает `Kid.room != room_R`. */
if (ch->curr_col < 0 || ch->curr_col > 8) return 0;
return (uint8_t)(get_tile(ch->curr_col, ch->curr_row) == TILE_GATE);
}
/* СТАРТ/РЕСТАРТ уровня: забыть фазы, НЕ сдавая их в room_modif — в
* оригинале всё состояние сносит сам load_level (seg003:57). Без этого
* pop_loose_leave_room у первого же enter_room занесла бы в только что
* восстановленный уровень дрожь из прошлой жизни: pop_trob_reset уже
* отработал, room_modif свежий, а pop_loose_modif — ещё старый. */
void pop_loose_forget(void) __banked
{
int i;
for (i = 0; i < 30; i++) pop_loose_modif[i] = 0;
for (i = 0; i < 10; i++) pop_ceil_modif[i] = 0;
}
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
* индексируются позицией тайла и не должны утечь из старой комнаты в новую.
* Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и
* должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed).
*
* Вместо нулей забираем фазу НОВОЙ комнаты из room_modif — обратная
* половина pop_loose_leave_room: расшатанная и брошенная плита, к которой
* Кид вернулся, продолжает дрожать с той же фазы, а не с покоя. trob,
* который крутил её заочно, снимет сам animate_loose (room == cur_room). */
void pop_loose_reset(void) __banked
{
uint8_t *mod;
int i;
mod = g_room ? pop_trob_modif(g_room) : 0;
for (i = 0; i < 30; i++) {
pop_loose_modif[i] = (mod && g_fg && (g_fg[i] & 0x1F) == TILE_LOOSE)
? mod[i] : 0;
if (pop_loose_modif[i]) loose_any = 1; /* взвод гейта — см. loose_any */
}
mod = (g_above && g_link_u) ? pop_trob_modif(g_link_u) : 0;
for (i = 0; i < 10; i++) {
pop_ceil_modif[i] = (mod && (g_above[i] & 0x1F) == TILE_LOOSE)
? mod[20 + i] : 0;
if (pop_ceil_modif[i]) loose_any = 1; /* взвод гейта — см. loose_any */
/* ...и СНЯТЬ заочный trob: плиту-потолок отсюда снова ведёт
* pop_loose_tick. Без этого её крутили бы ДВОЕ — trob по
* room_modif и мы по pop_ceil_modif, — и она проваливалась бы
* дважды (animate_loose снимает сам только trob'ы cur_room, а
* комната сверху — не cur_room). */
if (pop_ceil_modif[i]) pop_add_trob(g_link_u, (uint8_t)(20 + i), -1);
}
pop_ceil_fell = 0;
pop_debris_at = 0;
pop_loose_landed = 0;
pop_loose_mob_room_changed();
}
/* remove_loose (seg007:0EB8) для ОДНОГО тайла, наружу — ради честного
* loose_fall: падающий кусок сбивает плиту НЕМЕДЛЕННО, в том же кадре
* (pop_room.c, mob_break_loose). Здесь, а не в pop_room, потому что тут
* живут и рабочая копия тайлов комнаты (g_fg), и фазы плит.
*
* Перерисовываем ДВЕ клетки — саму и соседа справа. Передний торец пола
* заезжает в соседний тайл (правая грань соседа всегда в backtable), и без
* второй пометки от исчезнувшей плиты оставался висеть торец: тайл пустой,
* а его передняя грань нарисована. Тем же приёмом, что pop_skel_wake_tile
* и что set_redraw_full+set_wipe на curr_tilepos и curr_tilepos+1 в
* оригинале. */
void pop_loose_remove(uint8_t room, uint8_t tilepos) __banked
{
uint8_t *mod = pop_trob_modif(room);
if (tilepos >= 30) return;
if (room == g_room) {
g_fg[tilepos] = TILE_EMPTY;
pop_loose_modif[tilepos] = 0;
pop_set_redraw(tilepos, POP_RD_LOOSE_GONE, 2);
if ((tilepos % 10) < 9)
pop_set_redraw((uint8_t)(tilepos + 1), POP_RD_FLOOR, 2);
} else {
/* Комната не на экране, но её кромочная колонка видна в шве — те же
* снимки, что обновляет animate_loose. */
pop_neigh_dirty = 1;
}
/* Модификатор пустой клетки = ТИП УРОВНЯ (по нему draw_tile_right
* выбирает blueline у дыры), тайл в странице уровня — сразу. */
if (mod) mod[tilepos] = pop_palace;
pop_level_set_tile(room, tilepos, TILE_EMPTY);
}
/* check_fall_flo (seg000:1317) — СПЕЦСОБЫТИЕ уровня 13: гряда плит над
* комнатой сыплется на Кида сама, вразнобой.
*
* Как это устроено в оригинале: при входе в комнату 23 или 16 шести плитам
* ряда 2 комнаты СВЕРХУ раздаётся ОТРИЦАТЕЛЬНЫЙ модификатор −(rnd & 15).
* Отрицательное значение — это старший бит, то есть для animate_loose оно
* выглядит как «тряска», а на уровне 13 тряска намеренно не гасится
* (спецкейс в pop_loose_tick). Фаза считает вверх, доходит до нуля и
* дальше идёт обычным отсчётом до провала — получается разная задержка у
* каждой плиты.
*
* Плиты ряда 2 верхней комнаты у нас — это pop_ceil_modif (плиты-потолки),
* так что раздаём фазу именно туда.
*
* ОДНО отличие от оригинала, вынужденное представлением: у нас фаза 0 = «не
* анимируется», плита с такой фазой в цикле пропускается вовсе. Значит
* счёт, дойдя до нуля, оборвался бы — поэтому, во-первых, тик перескакивает
* ноль (см. pop_loose_tick), а во-вторых, стартовое значение берётся на
* единицу «отрицательнее» (−n−1): вместе это даёт ровно ту же суммарную
* задержку n + 11 кадров, что и у оригинала.
*
* Звать при входе в комнату, после pop_loose_reset (тот забирает фазы из
* room_modif) — как check_the_end зовёт его после start_chompers. */
static pop_rnd_t loose_seed; /* свой домен последовательности, см. pop_geom.h */
void pop_check_fall_flo(void) __banked
{
uint8_t col;
if (pop_current_level != POP_LOOSE_TILES_LEVEL) return;
if (g_room != POP_LOOSE_TILES_ROOM1 && g_room != POP_LOOSE_TILES_ROOM2) return;
if (!g_above || !g_link_u) return;
for (col = POP_LOOSE_TILES_FIRST - 20; col <= POP_LOOSE_TILES_LAST - 20; col++) {
uint8_t t = g_above[col];
/* Те же отсевы, что в make_loose_fall: не loose и «solid» loose
* пропускаются молча (в комнате 1 тайл 24 пустой — диапазон 22..27
* намеренно надмножество), уже идущий отсчёт не перебиваем. */
if ((t & 0x1F) != TILE_LOOSE || (t & 0x20)) continue;
if ((int8_t)pop_ceil_modif[col] > 0) continue;
pop_ceil_modif[col] =
(uint8_t)(-(int8_t)(pop_prandom(&loose_seed, 255) & 0x0F) - 1);
pop_set_redraw_above(col, POP_RDA_CEIL, 1);
}
}
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
* (кадры 5..14) не бьёт (кроме уровня 13 — его тут нет); в висе/подъёме тоже
* (action >= hang_climb, кроме turn). Иначе −1 HP: смерть → seq_22 (crushed),
* живой → seq_52 (плита придавила: Kid приседает), с отступом на 2 назад,
* если сзади пусто. Кадр 109 (присед) последовательность не меняет. */
#define SEQ_52_LOOSE_FELL_ON_KID 52
#define FRAME_109_CROUCH 109
static void fell_on_your_head(void)
{
uint8_t frame = Char.frame, action = Char.action;
/* СПЕЦКЕЙС УРОВНЯ 13 (seg007:1218): там плита бьёт и В БЕГЕ. На прочих
* уровнях бегущего она пропускает — иначе гряда падающих плит, сквозь
* которую и надо пробегать, не представляла бы никакой угрозы. */
if ((pop_current_level == POP_LOOSE_TILES_LEVEL || frame < 5 || frame >= 15) &&
(action < ACT_HANGCLIMB || action == ACT_TURN)) {
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
if (pop_take_hp(1)) { /* «брызги» даст pop_do_delta_hp */
pop_char_set_seq(SEQ_22_CRUSHED);
play_seq();
determine_col();
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
} else if (frame != FRAME_109_CROUCH) {
if (get_tile_behind_char() == TILE_EMPTY)
Char.x = (uint8_t)char_dx_forward(-2);
pop_char_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
play_seq();
determine_col();
}
}
}
/* check_loose_fall_on_kid (seg007:1192): падающий кусок в той же колонке и
* его низ уже в полосе 30px над ногами Kid → удар. */
static void check_loose_fall_on_kid(void)
{
int mcol, my;
if (pop_kid_dead) return;
/* Ни одного куска в воздухе — не платим за банковый трамплин в
* pop_loose_mob_pos с обходом 14 слотов (замер 11/15: 5 868 тактов на
* комнату, где не падает ничего). */
if (!pop_mob_busy) return;
if (!pop_loose_mob_pos(&mcol, &my)) return;
/* Своё окно Char, как в оригинале (seg007:1199 — loadkid/savekid внутри
* самой функции): зовут нас из pop_loose_tick, а тот идёт в главном
* цикле вне окон, и в Char к этому моменту лежит СТРАЖ (он тикает
* последним). Плита на голову — про Кида. */
pop_loadkid();
if (Char.curr_col == (int8_t)mcol &&
my < (int)Char.y && (int)Char.y - 30 < my) {
fell_on_your_head();
pop_savekid();
}
}
/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ
* (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */
void pop_loose_tick(void) __banked
{
uint8_t pos;
/* Массивы обходим УКАЗАТЕЛЕМ, а не индексом. С `int pos` SDCC держал
* счётчик в IX-фрейме, каждую итерацию заново складывал 16-битный адрес
* элемента, клал его в локал и тут же вычитывал обратно парами pop/push:
* 40 холостых итераций стоили 27 936 тактов (замер 2026-08-13). Ходьба
* указателем убирает и адресную арифметику, и IX-фрейм. */
uint8_t *lm = pop_loose_modif;
/* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80
* нет аппаратного деления, и SDCC выносил оба деления (__divuchar/
* __moduchar) в начало КАЖДОЙ итерации (общее подвыражение аргументов
* вызовов), т.е. 30*2=60 вызовов деления за кадр — даже в покое и в
* комнатах без loose-плит (условия if к тому моменту ещё не проверены).
* Инкремент+сравнение считают row/col без деления. */
uint8_t row = 0, col = 0;
pop_dbg_m9(); /* ЗАМЕР: вход pop_loose_tick */
/* Гейт холостого хода: если ни одна фаза не взведена, оба цикла (30 + 10
* позиций) пропускаем целиком — замер 11/15 дал 9 852 такта на комнату,
* где loose-плит нет вовсе. Флаг ставят места записи фазы, снимаем его
* здесь и только по факту прохода, в котором не осталось ни одной живой
* фазы (см. объявление loose_any). */
if (loose_any) {
uint8_t found = 0;
for (pos = 0; pos < 30; pos++, lm++) {
if (*lm != 0) {
uint8_t m = ++*lm;
/* Отложенный старт уровня 13 досчитал до нуля. Ноль у нас
* означает «не анимируется», и плита застряла бы навсегда —
* перескакиваем на 1. Стартовое значение выбрано с учётом
* этого перескока (см. pop_check_fall_flo). */
if (m == 0) m = *lm = 1;
if (m & 0x80) { /* тряска (do_knock) */
/* СПЕЦКЕЙС УРОВНЯ 13 (seg007:823): фазу со старшим битом НЕ
* гасим и не трясём. На нём check_fall_flo раздаёт плитам
* ОТРИЦАТЕЛЬНЫЙ старт (0xF0..0xFF) как отложенный таймер, и
* обычная ветка «тряска кончилась на 0x84» убила бы его в
* первом же кадре — плита не упала бы никогда. Пропущенная
* инкрементом фаза сама дойдёт до 0x00, там старший бит
* снимется, и дальше пойдёт обычный отсчёт до провала. */
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
/* ждём молча */
} else if (m >= 0x84) { /* конец тряски: сброс + покой */
*lm = 0;
/* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
* правая грань loose живёт в соседе). ОБЕ страницы —
* иначе на одной застывает дрожащий кадр → мерцание. */
pop_set_redraw(pos, POP_RD_LOOSE, 2);
} else {
pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */
}
} else if (m >= 11 && pop_loose_fell) {
/* Сигнал прошлого провала ещё не разобран главным циклом (он
* ОДИН на кадр) — придержим плиту кадр, откатив фазу к 10.
* Иначе из двух плит, провалившихся в одном кадре, дыру
* запекала бы только вторая, а первая оставалась нарисованной
* целой (уровень 13: check_fall_flo роняет гряду разом). */
*lm = 10;
} else if (m >= 11) { /* loose_floor_delay — падение */
/* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
* Debris появляется ТОЛЬКО там, где приземлился падающий
* кусок (mob), т.е. рядом ниже. Loose в нижнем ряду (2,6)
* — кусок улетает из комнаты вниз, на месте остаётся пусто
* (стена-вниз от тайла снизу + чёрный фон). */
g_fg[pos] = TILE_EMPTY;
/* remove_loose возвращает ТИП УРОВНЯ, и вызывающий кладёт
* его модификатором пустой клетки (seg007:846/1083): по нему
* draw_tile_right выбирает blueline_fram1 у дыры. */
pop_trob_modif(g_room)[pos] = pop_palace;
*lm = 0;
pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2); /* запечь пусто, обе стр. */
/* ...и СОСЕДА СПРАВА: передний торец пола заезжает в его
* клетку, и без этой пометки он оставался висеть над пустым
* тайлом. То же, что делает pop_skel_wake_tile. */
if ((pos % 10) < 9)
pop_set_redraw((uint8_t)(pos + 1), POP_RD_FLOOR, 2);
pop_loose_mob_spawn(row, col); /* отрыв падающего куска */
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
} else { /* отсчёт 1..10 — дрожащий кадр */
pop_set_redraw(pos, POP_RD_LOOSE, 1);
}
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
}
if (++col == 10) { col = 0; row++; } /* row/col без деления */
}
/* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры
* рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в
* НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */
lm = pop_ceil_modif; /* тот же указательный обход */
for (col = 0; col < 10; col++, lm++) {
if (*lm != 0) {
uint8_t m = ++*lm;
if (m == 0) m = *lm = 1; /* см. pop_loose_tick выше */
if (m & 0x80) { /* тряска от сотрясения (do_knock) */
/* Тот же спецкейс уровня 13, и здесь он ГЛАВНЫЙ: плиты,
* которым check_fall_flo раздал отложенный старт, лежат
* именно в ряду 2 комнаты СВЕРХУ, то есть ведёт их этот
* цикл, а не соседний. */
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
/* ждём молча */
} else if (m >= 0x84) { *lm = 0;
pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
} else if (m >= 11 && pop_ceil_fell) {
*lm = 10; /* сигнал занят — кадр подождём */
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
*lm = 0;
if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */
pop_set_redraw_above(col, POP_RDA_CEIL_GONE, 2); /* колодец, обе стр. */
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
} else {
pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */
}
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
}
}
loose_any = found;
}
pop_dbg_m10(); /* ЗАМЕР: оба цикла по тайлам пройдены */
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
pop_dbg_m11(); /* ЗАМЕР: pop_loose_mob_tick сделан */
check_loose_fall_on_kid(); /* seg007:1192 — плита падает Киду на голову */
pop_dbg_m12(); /* ЗАМЕР: check_loose_fall_on_kid сделан */
/* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД
* разбором нового: иначе при двух плитах, севших подряд, land_bake
* затирался новым и на одной странице оставался старый пол. */
if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */
uint8_t pos = (uint8_t)(pop_loose_landed - 1);
uint8_t r = 0, c = pos;
uint8_t tt = (uint8_t)(g_fg[pos] & 0x1F); /* тайл, НА который села плита */
pop_loose_landed = 0;
while (c >= 10) { c -= 10; r++; }
/* Плита, севшая на КНОПКУ, её нажимает (seg007:11E8, ветки
* tiles_15_opener / tiles_6_closer). Тонкость: на opener оригинал
* подставляет button_type = tiles_14_debris, а это у trigger_gate
* отдельная ветка «открыть НАСОВСЕМ» (возврат 2 -> animate_door
* доводит створку и ставит modifier 0xFF, после чего ворота
* заморожены и обычный opener их больше не трогает). Сама кнопка
* съедается: тайл станет щебнем ниже, нажаться второй раз нечему.
* Модификатор (индекс LINKLOC) тайл не меняет, берём его как есть. */
if (tt == TILE_OPENER || tt == TILE_CLOSER)
pop_trigger_button(g_room, pos,
(uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
pop_trob_modif(g_room)[pos]);
/* На факеле — отдельный тайл «факел с щебнем» (seg007:1067). */
g_fg[pos] = (uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
? TILE_TORCH_DEBRIS : TILE_DEBRIS);
/* Перерисовка ПО ПЛОЩАДИ ПОЛА (не LOOSE_GONE: та стирает и верх стены
* ряда ниже — оставался чёрный прямоугольник под тайлом). Обе
* страницы дабл-буфера. */
pop_set_redraw(pos, POP_RD_FLOOR, 2);
do_knock(r); /* удар плиты трясёт loose этого ряда */
pop_debris_at = (uint8_t)(pos + 1); /* сигнал приложению (персистентность) */
}
}
/* ---- clip_char (seg006:1749), первый блок: обрезка спрайта СВЕРХУ ----- *
* Оригинал перед add_objtable кладёт в objtable прямоугольник клипа; блиттер
* рисует персонажа только внутри него. Верхняя граница: если тайл НАД
* головой (ряд char_top_row) — стена или пол, всё выше линии своей полосы
* (y_clip[curr_row+1]) не рисуется. Без этого Kid, ушедший головой под пол
* (спуск с кромки/кнопки, вис), просвечивает сквозь щели кладки.
*
* Хитрость исходника: `X == wall || tile_is_floor(curr_tile2)` — curr_tile2
* ставит сам get_tile, т.е. это просто «тайл X = стена ИЛИ пол».
*
* Метрики габарита — порт set_char_collision (seg006:0723); в отличие от
* check_spike_below колонки берутся get_tile_div_mod БЕЗ 7.
*
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
/* Вторая половина ветки двери уровня из clip_char (seg006:1231):
* obj_clip_right = leveldoor_right — ЭКРАННЫЙ X, правее которого спрайт не
* рисуется. 0 = обрезки нет.
*
* Почему обрезка, а не fore-слой: створка и косяк уходят в оригинале
* ЦЕЛИКОМ в backtable (draw_leveldoor, все add_backtable), то есть рисуются
* ПОД персонажем и перекрыть его не могут. Единственный способ спрятать
* поднимающегося по лестнице — срезать сам спрайт. */
int pop_clip_char_right(void) __banked
{
if (Char.frame >= 224 && Char.frame < 229)
return pop_leveldoor_right;
return 0;
}
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 };
int wh = ((int)w + 1) >> 1; /* char_width_half */
int xl = obj_x / 2 + 58; /* char_x_left */
int xr, top_y, clip_y;
int8_t cL, cR, trow, clip_row;
uint8_t t;
/* Кадры подъёма по лестнице двери уровня (clip_char, seg006:1231):
* персонаж режется по низу поднятой створки. ВНИМАНИЕ: комментарий в
* SDLPoP говорит «frames 217..228», а КОД проверяет
* `>= frame_224_exit_stairs_8`, то есть 224..228 — портируем по коду.
* Правую кромку отдаёт pop_clip_char_right (парная половина ветки). */
if (Char.frame >= 224 && Char.frame < 229)
return pop_leveldoor_ybottom + 1;
if (Char.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; /* char_x_right */
top_y = obj_y - (int)h + 1; /* char_top_y */
if (top_y >= 192) top_y = 0; /* весь спрайт ниже комнаты */
trow = pop_y_to_row((int16_t)top_y); /* char_top_row */
cL = get_tile_div_mod(xl); if (cL < 0) cL = 0;
cR = get_tile_div_mod(xr); if (cR > 9) cR = 9;
t = get_tile(cL, trow);
if (t != TILE_WALL && !tile_is_floor(t)) return 0;
/* кадр 79 (прыжок вверх) / 81 (зацеп) в стойке клипятся и по одной грани */
if (!(Char.action == 0 && (Char.frame == 79 || Char.frame == 81))) {
t = get_tile(cR, trow);
if (t != TILE_WALL && !tile_is_floor(t)) return 0;
}
clip_row = (int8_t)(Char.curr_row + 1);
if (clip_row < 0 || clip_row > 4) return 0;
clip_y = y_clip[clip_row];
/* ряд 0 — клип безусловный; иначе только когда линия реально режет спрайт */
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < top_y))
return clip_y;
return 0;
}
/* check_leave_below (порт leave_room, seg002: leave_dir=3 при chary>=211):
* Kid пересёк нижнюю границу комнаты (провал в дыру ИЛИ спуск-зацеп/climbdown
* — единый триггер по y). Репроекция в кадр комнаты снизу (goto_other_room
* down: y-=189, curr_row=y_to_row_mod4) + сигнал pop_fell_out — приложение
* решит: переход (links.down) или выпал из уровня (нет комнаты снизу). */
static void check_leave_below(void)
{
/* leave_room (seg002:0504) проверяет ВВЕРХ ПЕРВЫМ: персонаж не в отскоке/
* полёте и его y (как signed) в полосе −16..10 у верха комнаты — он вылез
* в комнату сверху (через колодец от упавшей плиты). Порядок важен: при
* подтягивании y ≈ 248, и проверка «ниже комнаты» (y>=211) иначе
* перехватывает подъём и роняет Kid из уровня.
* goto_other_room(up): y += 189, curr_row = y_to_row_mod4. */
if (g_link_u && Char.action != ACT_BUMPED && Char.action != ACT_FREEFALL &&
Char.action != ACT_MIDAIR &&
(int8_t)Char.y < 10 && (int8_t)Char.y > -16) {
Char.y = (uint8_t)(Char.y + 189);
Char.curr_row = pop_y_to_row((int16_t)Char.y);
pop_leave_dir = 3;
return;
}
if (Char.y >= 211) {
Char.y = (uint8_t)(Char.y - 189);
Char.curr_row = pop_y_to_row((int16_t)Char.y);
pop_fell_out = 1;
}
}
/* check_leave вбок (порт leave_room, seg002): передний край персонажа
* (char_dx_forward(cur_frame.dx) = char_x_left при взгляде влево / char_x_right
* вправо) сравнивается с кромками комнаты. Взгляд вправо: >=201 → right,
* <=57 → left; взгляд влево: <=54 → left, >=198 → right. goto_other_room:
* x∓140 (ширина комнаты), пересчёт колонки; сигнал pop_leave_dir (2=right,
* 1=left). Уход только если сосед есть (иначе кромка = стена, check_bumped
* удержит у шва). */
static void check_leave(void)
{
int fx;
uint8_t f = Char.frame;
/* seg002 leave_room: комнату НЕ покидаем во время разворота (action turn),
* подъёма-с-зацепа (climb, кадры 135..149), вставания из приседа
* (110..119) и — ОТДЕЛЬНО ВАЖНОЕ — БОЯ МЕЧОМ (150..162 и 166..168).
*
* Без первых трёх разворот у правого шва (взгляд флипается право→лево,
* char_x_left проскакивает >=198) телепортирует Kid сквозь закрытые
* ворота в соседнюю комнату (leave_room возвращает 1).
*
* Без кадров МЕЧА бой у шва превращается в мельтешение: отступая и
* наступая, Kid пересекает границу почти каждый кадр, и комната
* перерисовывается туда-обратно — драться невозможно (найдено
* пользователем 2026-08-18, уровень 8, шов комнат 24/18). Оригинал
* пускает смену комнаты только на «легальном» кадре вроде 170 (стойка
* с мечом), а вся боевая анимация уход запрещает. Побочный эффект в
* оригинале известен и даже эксплуатируется: Trick 35 «retreat without
* leaving the room» — определённым ритмом «назад» из комнаты можно
* пятиться, не переключая её вовсе (в SDLPoP это чинит опциональный
* FIX_RETREAT_WITHOUT_LEAVING_ROOM, по умолчанию ВЫКЛЮЧЕННЫЙ; мы, как
* обычно, портируем оригинал, а не его исправления).
*
* NB: 135..149 и 150..162 смежны, поэтому слиты в один диапазон —
* условие то же, а сравнений на одно меньше. */
if (Char.action == ACT_TURN ||
(f >= 110 && f < 120) ||
(f >= 135 && f < 163) || /* подъём-с-зацепа + вся боевая анимация */
(f >= 166 && f < 169)) /* стойка «замер, меч наготове» */
return;
/* Передний край — ТОЛЬКО через char_x_forward_edge (obj_x-арифметика
* set_char_collision): в зоне шва (<=57) он на пиксель правее чистого
* Char.x+dx, и Kid, осевший у ворот на x=57, порога leave-left НЕ достигает
* (эталон SDLPoP: экран не переключается, BUG-SEAM-PINGPONG). */
fx = char_x_forward_edge();
if (Char.direction >= 0) { /* взгляд вправо: fx = char_x_right */
uint8_t t9 = get_tile(9, Char.curr_row);
if (g_link_r && fx >= 201 &&
t9 != TILE_DOORTOP_FLOOR && t9 != TILE_DOORTOP) {
/* leave_room (seg002): под doortop у правой кромки выхода нет */
Char.x = (uint8_t)(Char.x - 140); determine_col(); pop_leave_dir = 2;
} else if (g_link_l && fx <= 57) {
Char.x = (uint8_t)(Char.x + 140); determine_col(); pop_leave_dir = 1;
}
} else { /* взгляд влево: fx = char_x_left */
if (g_link_l && fx <= 54) {
Char.x = (uint8_t)(Char.x + 140); determine_col(); pop_leave_dir = 1;
} else if (g_link_r && fx >= 198) {
Char.x = (uint8_t)(Char.x - 140); determine_col(); pop_leave_dir = 2;
}
}
}
/* ---- Пики (spike, seg006/seg007) ---------------------------------- *
* Состояние пики в room_modif[g_room][tilepos] (pop_trob), автомат —
* animate_spike (pop_trob). Здесь: триггер выдвижения (check_spike_below),
* решение о смерти (check_spiked) + spiked(). */
/* start_anim_spike (seg007:08E3): взвести выдвижение, если пика в покое
* (modif 0 → trob type1) или ушла не полностью (modif<0, кроме 0xFF disabled
* → мгновенно 0x8F «вышли»). */
static void start_anim_spike(uint8_t tilepos)
{
uint8_t *mod = pop_trob_modif(g_room);
int8_t old;
if (!mod) return;
old = (int8_t)mod[tilepos];
if (old <= 0) {
if (old == 0)
pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */
else if ((uint8_t)old != 0xFF)
mod[tilepos] = 0x8F;
}
}
/* is_spike_harmful (seg007:1178): 0/-1 → 0 (безопасно); <0 → 1; 1..4 → 2;
* >=5 → 0. */
static int is_spike_harmful(uint8_t tilepos)
{
int8_t m = (int8_t)pop_trob_modif(g_room)[tilepos];
if (m == 0 || m == -1) return 0;
if (m < 0) return 1;
if (m < 5) return 2;
return 0;
}
/* spiked (seg005:0888): напоролся — пика становится безвредной (0xFF),
* выровнять по колонке, pop_take_hp(100) = смерть, seq_51 (импейл). */
static void spiked(uint8_t tilepos)
{
pop_trob_modif(g_room)[tilepos] = 0xFF;
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.x = (uint8_t)(pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
Char.x = (uint8_t)char_dx_forward(8);
Char.fall_y = 0;
pop_take_hp(100); /* (TODO: sound_48_spiked) */
pop_char_set_seq(SEQ_51_SPIKED);
play_seq();
determine_col();
/* pop_kid_dead — НАШ флаг респавна, он про Кида. У прочих персонажей
* смерть видна по обнулившимся HP: play_guard ставит alive = 0, а
* дальше их доигрывает общий control() (seq_71_dying). */
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
}
/* check_spiked (seg006:0658): под персонажем пика И вредна И кадр
* бега(7..14)/старта прыжка(34..39) при harmful>=2, ЛИБО кадр приземления
* (43/26) при harmful!=0 → smert'. */
static void check_spiked(void)
{
uint8_t frame = Char.frame, tp;
int h;
if (Char.curr_col < 0 || Char.curr_col > 9 || Char.curr_row < 0 || Char.curr_row > 2)
return;
if (get_tile_at_char() != TILE_SPIKE) return;
tp = (uint8_t)(Char.curr_row * 10 + Char.curr_col);
h = is_spike_harmful(tp);
if ((h >= 2 && ((frame >= FRAME_7_RUN && frame < 15) ||
(frame >= FRAME_34_RUNJUMP_START && frame < 40))) ||
((frame == FRAME_43_RUNJUMP_LAND || frame == FRAME_26_STANDJUMP_LAND) && h != 0))
spiked(tp);
}
/* check_spike_below (seg006:1199): для каждой колонки РЕАЛЬНОГО габарита
* спрайта (char_x_left..char_x_right, порт set_char_collision) скан ВНИЗ от
* curr_row через НЕ-floor тайлы; встретил пику → start_anim_spike. Габарит,
* а не «curr_col + колонка спереди»: иначе пики выдвигаются на тайл раньше
* (когда тело Кида ещё не над шахтой). Метрики кадра — pop_cd (габарит
* последнего нарисованного кадра; отставание в 1 кадр несущественно, ошибка
* в позднюю сторону). */
static void check_spike_below(void)
{
const pop_cdraw_t *cd = CD_ACT;
int wh = ((int)cd->fpw + 1) >> 1; /* char_width_half */
int xl = cd->fpx / 2 + 58; /* char_x_left */
int xr;
int8_t c0, c1, col;
if (Char.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; /* char_x_right */
c0 = get_tile_div_mod_m7(xl);
c1 = get_tile_div_mod_m7(xr);
for (col = c0; col <= c1; col++) {
int8_t row = Char.curr_row;
if (col < 0 || col > 9) continue;
for (;;) {
uint8_t t = get_tile(col, row);
if (t == TILE_SPIKE) { start_anim_spike((uint8_t)(row * 10 + col)); break; }
if (!tile_is_floor(t) && row <= 2) { row++; continue; }
break;
}
}
}
/* chomped (seg004:07BF): персонажа перемололо. Тайл помечается кровью
* (0x80 в modif), персонаж выравнивается по колонке чомпера, теряет все HP
* и уходит в seq_54. Условие «кадр ещё не 178» — чтобы не перезапускать
* последовательность каждый кадр, пока труп лежит в челюстях.
*
* Кровь пишется ДАЖЕ у скелета: SDLPoP чинит это опцией
* fix_skeleton_chomper_blood, но в ВАНИЛИ она есть, и мы держим ваниль. */
static void chomped(uint8_t tilepos, int8_t col)
{
pop_trob_modif(g_room)[tilepos] |= 0x80;
if (Char.frame == FRAME_178_CHOMPED) return;
Char.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
Char.x = (uint8_t)char_dx_forward(7 - !Char.direction);
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
pop_take_hp(100); /* (TODO: sound_46_chomped) */
pop_char_set_seq(SEQ_54_CHOMPED);
play_seq();
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
}
/* check_chomped_kid (seg004:076B): по колонкам ряда — там, где персонаж
* перекрывает тайл ОБЕИМИ гранями (у оригинала curr_row_coll_flags == 0xFF,
* у нас оба бита) и этот тайл — СОМКНУТЫЙ чомпер (modif & 0x7F == 2),
* персонажа перемалывает. */
/* lo..hi — ОКНО, по которому посчитан flags: вне него слоты протухшие (там
* лежат значения прошлых кадров), и «== 3» дал бы фантомный перемол. */
static void check_chomped_flags(const uint8_t *flags, int8_t lo, int8_t hi)
{
int8_t row = Char.curr_row, col;
uint8_t tp;
if (row < 0 || row > 2) return;
if (lo < 0) lo = 0;
if (hi > 9) hi = 9;
for (col = lo; col <= hi; col++) {
if (flags[COLL_IDX(col)] != 3) continue;
if (get_tile(col, row) != TILE_CHOMPER) continue;
tp = (uint8_t)(row * 10 + col);
if ((pop_trob_modif(g_room)[tp] & 0x7F) == 2) chomped(tp, col);
}
}
static void check_chomped_kid(void) { check_chomped_flags(coll_curr, coll_lo, coll_hi); }
/* check_chomped_guard (seg004:0989) — у оригинала ОТДЕЛЬНОЕ тело: страж не
* проходит через check_collisions, поэтому там перекрытие считается прямо по
* координатам граней (check_chomped_here). У нас та же формула уже живёт в
* get_row_collision_data, поэтому считаем флаги ряда во ВРЕМЕННЫЙ массив и
* идём общим телом. Именно во временный: coll_curr/above/below — это кадр
* КИДА, из них move_coll_to_prev берёт «прошлые» флаги на следующем кадре, и
* затирание сломало бы ему бамп. */
static void check_chomped_guard(void)
{
uint8_t flags[COLL_N];
if (Char.curr_row < 0 || Char.curr_row > 2) return;
set_char_collision();
calc_coll_window(); /* своё окно; coll_lo/hi Кида не трогаем */
get_row_collision_data(Char.curr_row, flags);
check_chomped_flags(flags, win_lo, win_hi);
}
/* ---- Кадровая физика: одна и та же над Char, два входа --------------- *
* Оригинал держит физику ОДНИМ набором функций над Char, а кто именно в
* Char — решают окна loadkid/savekid и loadshad/saveshad (seg006:809).
* Мы теперь так же: kid_phys/guard_phys — это списки вызовов из
* play_kid_frame (seg000:1209) и play_guard_frame (seg000:1246), а сами
* проверки общие. Списки отличаются ровно тем, чем в оригинале. */
static void kid_phys(void)
{
if (grab_timer) grab_timer--;
/* do_timers (seg003:0517): срок действия пера тикает раз в игровой кадр.
* Место — у Кида, а не у стража: эффект в оригинале снимается общим
* таймером главного цикла, а у нас единственный кадровый тик Кида. */
if (pop_feather) pop_feather--;
fall_accel();
fall_speed();
determine_col();
bump_into_opponent(); /* seg003: безоружный Кид отскакивает от стража */
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action();
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */
check_spiked(); /* seg006: напоролся на вредные пики → смерть */
check_chomped_kid(); /* seg004: перемололо в сомкнутых челюстях → смерть */
/* upside_down снимается смертью (seg000:1224: `if (upside_down &&
* Char.alive >= 0)`). У нас признак смерти — pop_kid_dead, ставится
* там же, где оригинал переводит alive в >= 0. */
if (pop_kid_dead && pop_upside_want) { pop_upside_want = 0; pop_upside_dirty = 1; }
if (pop_kid_dead) return; /* только что умер — без leave/knock в этот кадр */
pop_check_knock(); /* seg003: сотрясение приземления → тряска loose в ряду */
/* exit_room_timer (seg002 exit_room): 2 кадра ПОСЛЕ входа в комнату любой
* leave заблокирован. Без него транзиент отскока о ворота шва (seq_47
* роняет char_x за порог) сразу же выкидывает Kid в соседнюю комнату
* (ping-pong / баг #4). С таймером первый бамп попадает в окно блокировки,
* Kid оседает у грани (char_x стабильно > порога) — экран не перескакивает. */
if (pop_leave_timer) {
pop_leave_timer--;
} else {
check_leave_below(); /* seg002: y>=211 → переход в комнату снизу */
check_leave(); /* seg002: уход вбок → переход в соседнюю комнату */
}
}
/* play_guard_frame (seg000:1252..1266). Чего у стража НЕТ по сравнению с
* Кидом: bump_into_opponent, check_collisions/check_bumped (у него свой
* check_guard_bumped), check_gate_push, check_knock и весь уход из комнаты.
* check_chomped_guard — то же тело, что у Кида (у нас check_chomped). */
static void guard_phys(void)
{
fall_accel();
fall_speed();
determine_col();
check_guard_bumped();
check_action();
check_press(); /* страж тоже давит кнопки и проваливает loose */
check_spike_below();
check_spiked();
check_chomped_guard();/* seg000:1265 */
}
/* Физика КИДА — своё окно Char: главный цикл зовёт нас вне окон, а физика
* работает с активным персонажем. Сохраняем ПОЛНОСТЬЮ (pop_savekid_and_opp):
* физика меняет персонажу всё — координаты, ряд/колонку, кадр, seq. */
void pop_phys_tick(void) __banked
{
/* РАННЕГО ВЫХОДА ПО pop_kid_dead ЗДЕСЬ НЕТ — и это принципиально.
* У оригинала play_kid_frame (seg000:1231) гейт ровно один, `Char.room
* != 0`: вся цепочка крутится и на ТРУПЕ. Стоял `if (pop_kid_dead)
* return;` — дёшево и почти незаметно, но вместе с физикой он выключал
* check_press, а тот на мёртвом персонаже делает совсем не то, что на
* живом (died_on_button — кнопка ломается насовсем). Наблюдалось как
* «Кид разбился на кнопке открытия решётки, а решётка закрылась»
* (уровень 7, комната 3, тайл 2,1): кнопка получала РОВНО ОДНО нажатие —
* в кадре самой смерти, потому что pop_kid_dead ставит land() уже внутри
* цепочки. Разбор — BUGS_CLOSED.md#died-on-button.
*
* «Труп не шевелится» держится не этим выходом, а тем же, чем в
* оригинале: кадр смерти не двигается сам (dx/dy нулевые, sequence
* кончился), а уход из комнаты и сотрясение отсечены внутри kid_phys. */
pop_loadkid_and_opp();
kid_phys();
pop_savekid_and_opp();
}
/* Физика СТРАЖА. Зовётся из главного цикла (резидент), а не из guards.c:
* цена та же — один трамплин на кадр, — но банк 1 не зовёт банк 3, а окно
* Char стоит копию 16 байт. */
void pop_guard_phys_tick(void) __banked
{
/* Тот же гейт, что в pop_guard_tick: dir_56_none = персонаж выключен
* (clear_char), физику ему тоже не крутим. */
if (Guard.charid == 0 || Guard.direction == DIR_56_NONE ||
Guard.room != g_room) return;
pop_loadshad_and_opp();
/* seg000:1254 — физика работает, ТОЛЬКО пока страж внутри комнаты: за
* пределами полосы персонаж просто перестаёт двигаться, а
* провалившегося ниже убирает check_guard_fallout (seg002:0241).
*
* Это значит «своим ходом из комнаты не уйдёт», но НЕ «остаётся в ней
* всегда»: когда комнату меняет КИД, exit_room (seg002:03C7) может
* перенести стража следом — follow_guard, если тот жив, с вынутым
* мечом, стоит у нужного края (x < 91 при уходе влево, x >= 165 при
* уходе вправо) и в целевой комнате нет своего живого стража. У нас
* follow_guard пока не портирован (GUARD-PHYS в TASKS_OPEN.md). */
if (Char.x >= 44 && Char.x < 211) {
pop_load_fram_det_col(); /* load_frame_to_obj + load_fram_det_col */
guard_phys();
}
pop_saveshad_and_opp();
}
/* ---- Зеркало уровня 4 (порт seg003:0798..08A9) ---------------------- *
*
* Тайл зеркала ставится в рантайме (pop_trob.c place_mirror), а «стеной
* слева» он уже был — wall_type = 2 портирован давно. Здесь вторая часть:
* что происходит ПОСЛЕ того, как Кид пролетел сквозь него беговым прыжком
* (флаг pop_jumped_mirror взводит is_obstacle выше по файлу). */
/* check_mirror_image (seg003:085B). МУТИРУЕТ Char НА МЕСТЕ: x отражается
* относительно оси зеркала, direction инвертируется. Возвращает
* distance_mirror — насколько персонаж вошёл в тайл (оригинал по нему
* решает, рисовать ли отражение; нам это понадобится на шаге «отражение»).
*
* Мутация Char здесь ЗАДУМАНА, а не побочный эффект: оригинал зовёт эту
* функцию из двух мест, и оба на неё рассчитывают — при рисовании отражения
* (в Char временная копия Кида, обратно она не сохраняется) и при рождении
* тени (мутированный Char уходит в слот Guard). */
static int8_t mirror_image(void)
{
/* +10 — смещение оси зеркала внутри тайла; в оригинале так и написано
* комментарием («I think 10 is the offset for mirror collision»). */
int xpos = pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10;
int distance = distance_to_edge_weight();
if (Char.direction >= 0) distance = ~distance + TILE_SIZEX;
Char.x = (uint8_t)((xpos << 1) - (int)Char.x);
Char.direction = (int8_t)~Char.direction;
return (int8_t)(distance - 2);
}
/* jump_through_mirror (seg003:080A): рождение ТЕНИ. Кид копируется в слот
* Guard уже отражённым; HP меняются местами — у тени полное здоровье Кида, у
* самого Кида остаётся 1. Полосы HP отдельно перерисовывать не надо:
* pop_hp_draw сравнивает числа и смену увидит сам.
*
* savekid здесь НЕТ и в оригинале: собственные x/direction Кида остаются
* прежними, отражается только копия, уходящая в слот Guard. */
static void jump_through_mirror(void)
{
pop_loadkid();
pop_load_frame(); /* mirror_image смотрит на dx/flags КАДРА */
(void)mirror_image();
pop_jumped_mirror = 0;
Char.charid = CHARID_1_SHADOW;
/* play_sound(45) — звука в порте пока нет */
pop_saveshad();
guardhp_max = guardhp_curr = hitp_max;
hitp_curr = 1;
}
/* Линия клипа отражения СВЕРХУ — y_clip[curr_row+1] оригинала. Таблица та
* же, что в pop_clip_char_top; держим её здесь копией из четырёх значений,
* а не тащим наружу: ряд у отражения всегда 0..2. */
static int mirror_clip_top(int8_t row)
{
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
int8_t r = (int8_t)(row + 1);
if (r < 0 || r > 4) return 0;
return (int)y_clip[r];
}
void pop_check_mirror(void) __banked
{
if (pop_jumped_mirror == -1) { jump_through_mirror(); return; }
/* Отражение: пока Кид СТОИТ на тайле зеркала, рисуем его зеркальную
* копию (в оригинале — add_objtable(4)). Условия оригинала: тайл под
* персонажем — зеркало, distance_mirror >= 0 (вошёл в тайл достаточно
* глубоко) и его комната = отрисованная. */
if (Kid.room != g_room) return;
pop_loadkid();
if (get_tile_at_char() != TILE_MIRROR) return;
pop_load_frame();
if (mirror_image() >= 0)
pop_mirror_draw(mirror_clip_top(Char.curr_row));
/* Char остался ОТРАЖЁННЫМ — как в оригинале: обратно он не сохраняется,
* а следующий тик перезагружает окно из Kid. */
}