Files
Sprinter-SDCC/applications/PoP/roomtest/pop_map.c
T
snark13 6673279cef PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
  poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
  curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
  УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
  стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
  только в кадрах 150..189.  Пока ветка была одна (charid_2_guard), скелет
  получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
  комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.

Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
  guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
  Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
  меняется вместе со стражем.  Грабля: gfx_pal_load отдаёт указатель в BIOS,
  а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.

Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
  Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
  после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
  обратно.  +48 байт W2.

Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
  seg008:1B06) и расширение перебора объединённым прямоугольником
  «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
  кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
  (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
  «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
  вернувшейся кучей костей.

Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.

Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).

Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).

В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:11:03 +03:00

2185 lines
122 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 "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_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 и прочие настройки */
/* ---- Тайлы / действия / 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_SPIKE 2 /* пики */
#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */
#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_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
/* Кадры анимации для 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
#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 */
/* Состояние 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;
}
/* 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);
}
/* Урон — через общий pop_take_hp (резидент pop_guard.c): физика работает
* над Char, значит и HP обязаны убавляться у ТОГО, кто сейчас в Char. Своя
* копия take_hp здесь была Kid-only и правила hitp_curr мимо дельты. */
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked
{
uint8_t i;
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
/* fg соседей — прямо из уровня, одной скобкой доступа на обе комнаты.
* Держать ещё одну копию в приложении незачем: карта — единственный её
* потребитель (отрисовке швов хватает кромочных колонок pop_room_load). */
if (left || right) {
pop_level_access_begin();
for (i = 0; i < 30; i++) {
if (left) g_lcol[i] = pop_level_tile_raw(left, i);
if (right) g_rcol[i] = pop_level_tile_raw(right, i);
}
pop_level_access_end();
}
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и
* бампа на кадре входа нет). */
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 (бары выше головы). */
/* Живой модификатор (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;
return (uint8_t)((gm >> 2) + 6) >= (uint8_t)kid_fp_height();
}
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));
}
static uint8_t wall_type(uint8_t t)
{
switch (t) {
case 4: case 7: case 12: return 1; /* стена справа (gate/doortop) */
case 13: return 2; /* стена слева (mirror) */
case 18: return 3; /* chomper */
case 20: return 4; /* стена с обеих сторон */
default: return 0;
}
}
/* ---- Координаты ---------------------------------------------------- */
static int char_dx_forward(int8_t dx)
{
int d = dx;
if (Char.direction < 0) d = -d;
return (int)Char.x + d;
}
/* col = (xpos-58)/14 округл. вниз; g_obj_xl = позиция в тайле (0..13). */
static int8_t get_tile_div_mod(int xpos)
{
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);
/* Урон от падения (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();
/* start_chompers() — чомперов ещё нет (L3-CHOMP). */
/* 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 (get_tile_at_char() == TILE_WALL)
in_wall();
if (tile_is_floor(g_curr_tile))
land();
else if (Char.curr_row < 2)
inc_curr_row(); /* следующий ряд ВНУТРИ комнаты */
/* curr_row==2, нет пола: продолжает падать (fall_speed двигает y);
* пересечение нижней границы комнаты ловит check_leave_below (y>=211,
* порт leave_room) — общий путь для провала И спуска-зацепа. */
}
}
static void fall_accel(void)
{
if (Char.action == ACT_FREEFALL) {
int 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; }
/* ---- Прыжок вверх с зацепом (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_red;
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_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). */
void pop_proc_get_object(void) __banked
{
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_red = 1;
pop_flash_time = 2;
}
break;
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
pop_kid_add_life();
pop_flash_red = 1;
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
break;
case 5: /* «злое» зелье: −1 HP */
hitp_delta = -1;
pop_flash_red = 1;
pop_flash_time = 2;
break;
default: /* 3 перо / 4 переворот / 6 открыть —
* TODO вместе с их уровнями (7/9) */
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) */
}
}
/* ---- Диспетчер ----------------------------------------------------- */
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))
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) {
/* frames 102..105: check_grab — K4 */
} 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) */
}
/* ---- 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 */
/* «нет колонки»: НЕ −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 */
/* Смена комнаты: колонки перепроецированы, прошлые флаги недействительны.
* В оригинале это несовпадение prev_coll_room и curr_row_coll_room —
* на кадре входа бампа нет. */
static void pop_coll_invalidate(void) { coll_prev_row = COLL_ROW_NONE; }
/* 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)kid_fp_width() + 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; }
}
/* get_row_collision_data (seg004:0185) для одного ряда: бит0 = «персонаж
* достал левую грань стены колонки», бит1 = «правую». Оригинал считает
* колонки от col(char_x_left)1 до col(char_x_right)+2 и оставляет прочие
* как есть; мы считаем все четырнадцать — они дешёвые (индекс в массив +
* switch), зато не остаётся протухших ячеек (FIX_COLL_FLAGS SDLPoP). */
/* 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;
int8_t i;
if (coll_prev_row == COLL_ROW_NONE) {
for (i = 0; i < COLL_N; i++) coll_prev[i] = 3;
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; /* опустился на ряд */
for (i = 0; i < COLL_N; i++) coll_prev[i] = src[i];
}
static void get_row_collision_data(int8_t row, uint8_t *flags)
{
int8_t col;
for (col = COLL_C0; col < COLL_C0 + COLL_N; col++) {
uint8_t f = 0;
uint8_t wt = wall_type(get_tile(col, row));
if (wt) {
int coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
if (wall_dl[wt] + coll_left < coll_xr) f |= 1;
if (coll_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2;
}
flags[COLL_IDX(col)] = f;
}
}
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_prev_row = COLL_ROW_NONE; return; }
set_char_collision();
move_coll_to_prev(Char.curr_row);
coll_prev_row = Char.curr_row;
/* Порядок важен: prev уже забран, теперь три ряда пересчитываются
* НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). */
get_row_collision_data(Char.curr_row, coll_curr);
get_row_collision_data((int8_t)(Char.curr_row + 1), coll_below);
get_row_collision_data((int8_t)(Char.curr_row - 1), coll_above);
/* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
* в которой флаг перешёл 0→1. */
for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --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:239) — уровень 4, не портируем */
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 при отрисовке */
/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед 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;
}
/* 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) */
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 = (uint8_t)(Char.curr_row * 10 + Char.curr_col);
if (t == TILE_OPENER || t == TILE_CLOSER)
pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]);
else if (t == TILE_LOOSE) {
make_loose_fall(tp, 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;
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;
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;
}
}
/* 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 (при смене комнаты): pop_loose_modif/bake/rest
* индексируются позицией тайла и не должны утечь из старой комнаты в новую.
* Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и
* должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */
void pop_loose_reset(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;
}
pop_ceil_fell = 0;
pop_debris_at = 0;
pop_loose_landed = 0;
pop_loose_mob_room_changed();
}
/* 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;
if ((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;
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
{
int pos;
/* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80
* нет аппаратного деления, и SDCC выносил оба деления (__divuchar/
* __moduchar) в начало КАЖДОЙ итерации (общее подвыражение аргументов
* вызовов), т.е. 30*2=60 вызовов деления за кадр — даже в покое и в
* комнатах без loose-плит (условия if к тому моменту ещё не проверены).
* Инкремент+сравнение считают row/col без деления. */
uint8_t row = 0, col = 0;
for (pos = 0; pos < 30; pos++) {
if (pop_loose_modif[pos] != 0) {
uint8_t m = ++pop_loose_modif[pos];
if (m & 0x80) { /* тряска (do_knock) */
if (m >= 0x84) { /* конец тряски: сброс + покой */
pop_loose_modif[pos] = 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) { /* loose_floor_delay — падение */
/* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
* Debris появляется ТОЛЬКО там, где приземлился падающий
* кусок (mob), т.е. рядом ниже. Loose в нижнем ряду (2,6)
* — кусок улетает из комнаты вниз, на месте остаётся пусто
* (стена-вниз от тайла снизу + чёрный фон). */
g_fg[pos] = TILE_EMPTY;
pop_loose_modif[pos] = 0;
pop_set_redraw(pos, POP_RD_LOOSE_GONE, 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 (++col == 10) { col = 0; row++; } /* row/col без деления */
}
/* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры
* рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в
* НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */
for (col = 0; col < 10; col++) {
if (pop_ceil_modif[col] != 0) {
uint8_t m = ++pop_ceil_modif[col];
if (m & 0x80) { /* тряска от сотрясения (do_knock) */
if (m >= 0x84) { pop_ceil_modif[col] = 0;
pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
pop_ceil_modif[col] = 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 */
}
}
}
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
check_loose_fall_on_kid(); /* seg007:1192 — плита падает Киду на голову */
/* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД
* разбором нового: иначе при двух плитах, севших подряд, 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;
pop_loose_landed = 0;
while (c >= 10) { c -= 10; r++; }
g_fg[pos] = 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). Без этого гарда разворот у правого шва (взгляд флипается
* право→лево, char_x_left проскакивает >=198) телепортирует Kid сквозь
* закрытые ворота в соседнюю комнату (leave_room возвращает 1). */
if (Char.action == ACT_TURN ||
(f >= 135 && f < 150) ||
(f >= 110 && f < 120))
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 + колонка спереди»: иначе пики выдвигаются на тайл раньше
* (когда тело Кида ещё не над шахтой). Метрики кадра — kid_fp (габарит
* последнего kid_draw; отставание в 1 кадр несущественно, ошибка в позднюю
* сторону). */
static void check_spike_below(void)
{
int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */
int xl = kid_fp_obj_x() / 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;
}
}
}
/* ---- Кадровая физика: одна и та же над 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--;
fall_accel();
fall_speed();
determine_col();
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action();
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */
check_spiked(); /* seg006: напоролся на вредные пики → смерть */
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 появится вместе с чомперами (L3-CHOMP). */
static void guard_phys(void)
{
fall_accel();
fall_speed();
determine_col();
check_guard_bumped();
check_action();
check_press(); /* страж тоже давит кнопки и проваливает loose */
check_spike_below();
check_spiked();
}
/* Физика КИДА — своё окно Char: главный цикл зовёт нас вне окон, а физика
* работает с активным персонажем. Сохраняем ПОЛНОСТЬЮ (pop_savekid_and_opp):
* физика меняет персонажу всё — координаты, ряд/колонку, кадр, seq. */
void pop_phys_tick(void) __banked
{
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
pop_loadkid_and_opp();
kid_phys();
pop_savekid_and_opp();
}
/* Физика СТРАЖА. Зовётся из главного цикла (резидент), а не из guards.c:
* цена та же — один трамплин на кадр, — но банк 1 не зовёт банк 3, а окно
* Char стоит копию 16 байт. */
void pop_guard_phys_tick(void) __banked
{
if (Guard.charid == 0 || 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();
}