Files
Sprinter-SDCC/applications/PoP/roomtest/pop_map.c
T
Александр Петров e67117219f BUG-CTRL-FRAME-1: геометрия Кида считалась по кадру стража
cur_frame — один глобал на всех персонажей (как в оригинале), владелец —
тот, кто последним прошёл load_frame.  Последним в кадре тикает страж,
поэтому к моменту control() Кида там лежал кадр СТРАЖА, а через
kid_cur_dx/dy/flags по нему считается вся геометрия управления:
dx_weight -> determine_col -> distance_to_edge_weight -> get_edge_distance
-> выбор ветки в check_jump_up.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:55:43 +03:00

1823 lines
100 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 — ветка приземления с мечом */
/* ---- Тайлы / действия / 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_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 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_do_delta_hp раз в кадр — как в оригинале.
* Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и
* проверен до боёвки, и переводить его на дельту без нужды рискованно. */
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 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
* соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично
* (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах —
* иначе Kid, войдя в соседнюю комнату у самой кромки (curr_col=-1/10), стоял
* бы «в стене» уровня и in_wall выбрасывал бы его. ВТОРАЯ колонка нужна,
* когда Kid стоит В шве и смотрит НАРУЖУ: передняя колонка тогда −2 (или 11),
* и мнимая стена там выпихивала его сквозь решётку (баг разворота у ворот).
* Оригинал резолвит любую колонку через find_room_of_tile (seg006:005D).
* Задаются pop_map_set_edges. */
static uint8_t g_link_l, g_link_r, g_link_u, g_link_d;
static uint8_t g_lcol[6], g_rcol[6];
/* ---- Плита-ПОТОЛОК: 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);
}
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
static uint8_t take_hp(uint8_t count)
{
if (count >= hitp_curr) { hitp_curr = 0; return 1; }
hitp_curr = (uint8_t)(hitp_curr - count);
return 0;
}
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked
{
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; }
if (rcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_rcol[i] = rcol_fg[i]; }
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
* недействительны (в оригинале 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 == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */
g_curr_tile = g_lcol[row] & 0x1F;
else if (col == -2 && g_link_l) /* на тайл глубже в комнату слева (col8) */
g_curr_tile = g_lcol[3 + row] & 0x1F;
else if (col == 10 && g_link_r) /* правый шов: тайл col0 комнаты справа */
g_curr_tile = g_rcol[row] & 0x1F;
else if (col == 11 && g_link_r) /* на тайл глубже в комнату справа (col1) */
g_curr_tile = g_rcol[3 + row] & 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 == -1 && g_link_l) return pop_trob_modif(g_link_l)[row * 10 + 9];
if (col == 10 && g_link_r) return pop_trob_modif(g_link_r)[row * 10 + 0];
if (col >= 0 && col <= 9) return pop_trob_modif(g_room)[row * 10 + col];
return 0xFF;
}
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(Kid.curr_col, Kid.curr_row);
}
static uint8_t get_tile_infrontof_char(void)
{
g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
return get_tile(g_infrontx, Kid.curr_row);
}
/* ---- Тайлы НАД/ЗА персонажем (для зацепа/спуска, seg006) ----------- */
static uint8_t get_tile_above_char(void)
{
return get_tile(Kid.curr_col, (int8_t)(Kid.curr_row - 1));
}
static uint8_t get_tile_behind_char(void)
{
return get_tile((int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col),
Kid.curr_row);
}
static uint8_t get_tile_front_above_char(void)
{
g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
return get_tile(g_infrontx, (int8_t)(Kid.curr_row - 1));
}
static uint8_t get_tile_behind_above_char(void)
{
return get_tile((int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col),
(int8_t)(Kid.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 (Kid.direction < 0) d = -d;
return (int)Kid.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)
{
Kid.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 (Kid.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 (Kid.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 += (Kid.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 (Kid.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;
Kid.x = (uint8_t)char_dx_forward((int8_t)d);
determine_col();
get_tile_at_char();
}
/* ---- Падение ------------------------------------------------------- */
static void inc_curr_row(void) { Kid.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)(Kid.curr_row * 10 + Kid.curr_col);
} else if (distance_to_edge_weight() >= 12 &&
get_tile_behind_char() == TILE_SPIKE) {
int8_t bcol = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col);
if (bcol < 0 || bcol > 9) return 0;
tp = (uint8_t)(Kid.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;
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
if (fell_on_spikes()) return; /* упал на вредные пики — смерть */
/* к краю пола — чуть назад (как оригинал) */
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3)
Kid.x = (uint8_t)char_dx_forward(-3);
/* Урон от падения (seg005 land): 1 этаж — мягко; 2 этажа — −1 HP и
* присед; 3+ — насмерть. pop_kid_hurt = hitp_delta<0 оригинала: по нему
* приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt,
* seg003:0AFC). */
if (Kid.fall_y < 22) {
/* seg005:176 — ВЕТКА С МЕЧОМ, и она не косметика. seq_17_soft_land
* заканчивается ВЕЧНЫМ циклом на кадре 109 (`jmp softland_crouch` в
* seqtbl), из которого выводит только control_crouched(). А
* control() при `Char.sword == sword_2_drawn` до него не доходит:
* управление уходит в control_with_sword, который знает лишь кадр
* 171 (стойка с мечом). То есть Кид, упавший на этаж С ВЫНУТЫМ
* МЕЧОМ, в этот присед садится НАВСЕГДА — ровно то, что поймали на
* приёмке уровня 2 (BUG-LAND-SWORD-1). Оригинал такой позы не
* допускает: с мечом приземление даёт боевую стойку. */
if (Kid.sword == SWORD_2_DRAWN) {
seq = SEQ_63_ACTIVE_AFTER_FALL;
} else {
seq = SEQ_17_SOFT_LAND;
}
} else {
uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */
pop_kid_hurt = 1;
Kid.fall_x = Kid.fall_y = 0;
if (deadly || take_hp(1)) { /* 3+ этажа или последнее HP */
take_hp(100);
kid_set_seq(SEQ_22_CRUSHED);
pop_kid_play();
determine_col();
pop_kid_dead = 1;
return;
}
seq = SEQ_20_MEDIUM_LAND;
}
Kid.fall_x = Kid.fall_y = 0;
kid_set_seq(seq);
pop_kid_play();
determine_col();
}
static void start_fall(void)
{
uint8_t frame = Kid.frame, seq, tile;
inc_curr_row();
/* 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 seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
kid_set_seq(seq);
pop_kid_play();
determine_col();
/* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2).
* Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose)
* наш get_tile отдаёт WALL как СЕНТИНЕЛ кромки уровня, но в оригинале там
* КОМНАТА СНИЗУ (у дыры — пол/пусто, не стена). Без гарда in_wall() ложно
* срабатывал и откатывал X назад (6−d) → дайв падения смещался на тайл
* влево (Кид «клевал» рано над колонной вместо дыры). */
if (Kid.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)
Kid.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 && Kid.direction >= 0) return 0; /* верх двери, лицом вправо */
if (tile_is_floor(through)) return 0; /* сквозь пол нельзя */
if (g_curr_tile == TILE_DOORTOP_FLOOR && Kid.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)Kid.fall_y >= 32) return; /* падает слишком быстро */
if ((uint16_t)pop_y_land[Kid.curr_row + 1] > (uint16_t)(Kid.y + 25)) return;
old_x = Kid.x;
Kid.x = (uint8_t)char_dx_forward(-8);
determine_col();
if (!can_grab_front_above()) {
Kid.x = old_x; /* не за что — назад */
determine_col();
} else {
Kid.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
Kid.fall_y = 0;
kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
pop_kid_play();
determine_col();
grab_timer = 12; /* (TODO: sound grab) */
}
}
static void do_fall(void)
{
uint8_t nrow = (uint8_t)(Kid.curr_row + 1);
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Kid.y) {
check_grab(); /* ещё летит — попытка зацепа */
} else {
if (get_tile_at_char() == TILE_WALL)
in_wall();
if (tile_is_floor(g_curr_tile))
land();
else if (Kid.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 (Kid.action == ACT_FREEFALL) {
int fy = (uint8_t)Kid.fall_y + FALL_ACCEL;
if (fy > FALL_MAX) fy = FALL_MAX;
Kid.fall_y = (int8_t)fy;
}
}
static void fall_speed(void)
{
Kid.y = (uint8_t)(Kid.y + (uint8_t)Kid.fall_y);
if (Kid.action == ACT_FREEFALL) {
Kid.x = (uint8_t)char_dx_forward(Kid.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, Kid.curr_col);
if (d >= 0) return edge_classify(d);
}
t = get_tile_infrontof_char();
/* Верх двери лицом ВПРАВО — кромка (в оригинале ПЕРВОЙ проверкой, до
* wall_type: у doortop wall_type != 0). */
if (t == TILE_DOORTOP && Kid.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),
* и он остаётся на месте; шаг НА плиту — только со второго нажатия
* (Kid.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 — без), и выравнивает Kid.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) {
Kid.x = (uint8_t)char_dx_forward((int8_t)distance);
return SEQ_8_GRAB_STRAIGHT;
}
Kid.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)
Kid.x = (uint8_t)char_dx_forward((int8_t)(distance - 3));
col = get_tile_div_mod(dx_weight() - 6);
t = get_tile(col, (int8_t)(Kid.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();
Kid.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();
Kid.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();
}
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
* позицию держит 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 (+ выравнивание Kid.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) && Kid.direction == 0) ||
(t == TILE_GATE && Kid.direction != 0 &&
(uint8_t)(gate_modif(Kid.curr_col, (int8_t)(Kid.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 (Kid.action != ACT_HANGSTRAIGHT &&
(t == TILE_WALL ||
(Kid.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 (встать, конец спуска). Меняет Kid.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 ||
(Kid.direction < 0 && (at == TILE_DOORTOP_FLOOR || at == TILE_DOORTOP)))
Kid.x = (uint8_t)char_dx_forward(-7);
return SEQ_11_RELEASE_LEDGE_LAND;
}
/* down_pressed (seg005:04C7) tile-часть: 1 = обработано (НЕ приседать —
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
* приседать. Меняет Kid.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] = Kid.curr_col;
cols[1] = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col);
cols[2] = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
for (i = 0; i < 3; i++) {
if (get_tile(cols[i], Kid.curr_row) != TILE_LEVELDOOR_LEFT)
continue;
/* дверь найдена — дальше решает только её modif (см. выше) */
if (cols[i] < 0 || cols[i] > 9) return 0; /* дверь в шве: не наша */
if (pop_trob_modif(g_room)[Kid.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 уводит его влево, ИЗ проёма. */
Kid.x = (uint8_t)(pop_x_bump[cols[i] + FIRST_ONSCREEN_COLUMN] + 10);
Kid.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) {
Kid.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) &&
(Kid.direction >= 0 || get_tile_at_char() != TILE_GATE ||
(uint8_t)(gate_modif(Kid.curr_col, Kid.curr_row) >> 2) >= 6)) {
Kid.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 9));
kid_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;
Kid.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 (Kid.frame != FRAME_109_CROUCH) {
/* Ещё стоим: подровняться к предмету и присесть. */
int d = pop_edge_distance(); /* ставит g_edge_type */
if (g_edge_type != EDGE_FLOOR)
Kid.x = (uint8_t)char_dx_forward((int8_t)d);
if (Kid.direction >= 0)
Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
return 1;
}
{
uint8_t tp = (uint8_t)(Kid.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 = Kid.action, frame = Kid.frame;
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)
{
Kid.x = (uint8_t)char_dx_forward(-4);
if (Kid.action == ACT_FREEFALL) {
Kid.fall_x = 0;
} else {
kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */
pop_kid_play();
}
}
/* 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[Kid.curr_row + 1] - (int16_t)Kid.y) >= 15) {
bumped_fall(); /* высоко над полом — в воздухе */
return;
}
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; /* ПРИЖАТЬ к полу */
if (Kid.fall_y >= 22) { /* влетел быстро — только отжать */
Kid.x = (uint8_t)char_dx_forward(-5);
return;
}
Kid.fall_y = 0;
/* `if (Char.alive)` (seg004:0520) — у ЖИВОГО (alive = −1) сиквенс удара
* ставится ВСЕГДА, даже если персонаж уже в отскоке. Раньше здесь стоял
* наш guard `if (Kid.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 (!Kid.alive) return; /* труп — только выравнивание */
frame = Kid.frame;
/* Кадры прыжков и начала падения — «жёсткий» удар с приседанием
* (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */
if (frame == 24 || frame == 25 || (frame >= 40 && frame < 43) ||
(frame >= 102 && frame < 107))
kid_set_seq(SEQ_46_HARDBUMP);
else
kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */
pop_kid_play();
}
/* ---- 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).
*
* ОТЛИЧИЕ ОТ ОРИГИНАЛА (сознательное): он держит флаги ТРЁХ рядов (curr/
* above/below), чтобы при смене ряда «прошлые» флаги были настоящими. У
* нас ряд один: сменился ряд — считаем, что персонаж уже перекрывал всё
* (prev = 3), то есть на кадре смены ряда бампа нет, а на следующем он
* посчитается честно. Три ряда — это втрое больше get_tile каждый кадр;
* если где-то вылезет пропущенный бамп ровно на кадре приземления —
* вернуться сюда. */
/* Колонки считаем от −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 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 (Kid.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). */
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 (Kid.action == ACT_TURN) return;
/* Вне рядов комнаты (падение мимо пола) get_tile отдаёт WALL во всех
* колонках — считать по нему нечего. Стену в падении держит
* do_fall/in_wall, выход из комнаты — check_leave_below. */
if (Kid.curr_row < 0 || Kid.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; }
set_char_collision();
/* move_coll_to_prev (seg004:00DF) в нашем однорядном варианте. */
if (coll_prev_row == Kid.curr_row) {
for (col = 0; col < COLL_N; col++) coll_prev[col] = coll_curr[col];
} else {
for (col = 0; col < COLL_N; col++) coll_prev[col] = 3; /* «уже перекрывал» */
}
coll_prev_row = Kid.curr_row;
get_row_collision_data(Kid.curr_row, coll_curr);
/* Обход СВЕРХУ ВНИЗ, как в оригинале (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, Kid.curr_row);
if (t == TILE_POTION) return 0; /* зелье не бампит */
if (t == TILE_GATE) return (uint8_t)!gate_passable(col, Kid.curr_row);
if (t == TILE_CHOMPER) /* закрыт только при modif==2 */
return (uint8_t)(col >= 0 && col <= 9 &&
pop_trob_modif(g_room)[Kid.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 (Kid.alive >= 0) return; /* труп не бампит */
Kid.x = (uint8_t)((int)Kid.x + delta_x);
determine_col();
ct = g_curr_tile; /* тайл препятствия */
if (push_dir < 0) { /* толчок ВЛЕВО */
if (ct == TILE_WALL)
ct = get_tile((int8_t)(tcol - 1), Kid.curr_row);
} else { /* толчок ВПРАВО */
if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL)
ct = get_tile((int8_t)(tcol + 1), Kid.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 (Kid.direction != 0) 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 (Kid.direction >= 0) 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);
}
/* 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 (Kid.action == ACT_HANGCLIMB || Kid.action == ACT_HANGSTRAIGHT) return;
if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */
/* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb),
* т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку
* hang->hang_fall->seq_11 (правильное приземление). */
if (Kid.frame >= 87 && Kid.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 = Kid.frame, action = Kid.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 (Kid.curr_row == 0) {
/* Ряд 0: «сверху» — ряд 2 комнаты СВЕРХУ (в оригинале get_tile с
* row=1 уходит по links.up, find_room_of_tile seg006:005D).
* Плита-ПОТОЛОК; кнопок на потолке в этом тесте нет. */
int8_t c = Kid.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) */
} else if (get_tile_above_char() == TILE_LOOSE) {
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
}
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)(Kid.curr_row * 10 + Kid.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);
}
}
/* 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(Kid.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 = Kid.frame, action = Kid.action;
if ((frame < 5 || frame >= 15) &&
(action < ACT_HANGCLIMB || action == ACT_TURN)) {
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */
if (take_hp(1)) {
kid_set_seq(SEQ_22_CRUSHED);
pop_kid_play();
determine_col();
pop_kid_dead = 1;
} else if (frame != FRAME_109_CROUCH) {
if (get_tile_behind_char() == TILE_EMPTY)
Kid.x = (uint8_t)char_dx_forward(-2);
kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
pop_kid_play();
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;
if (Kid.curr_col != (int8_t)mcol) return;
if (my < (int)Kid.y && (int)Kid.y - 30 < my)
fell_on_your_head();
}
/* 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 (Kid.frame >= 224 && Kid.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 (Kid.frame >= 224 && Kid.frame < 229)
return pop_leveldoor_ybottom + 1;
if (Kid.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 (!(Kid.action == 0 && (Kid.frame == 79 || Kid.frame == 81))) {
t = get_tile(cR, trow);
if (t != TILE_WALL && !tile_is_floor(t)) return 0;
}
clip_row = (int8_t)(Kid.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 && Kid.action != ACT_BUMPED && Kid.action != ACT_FREEFALL &&
Kid.action != ACT_MIDAIR &&
(int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) {
Kid.y = (uint8_t)(Kid.y + 189);
Kid.curr_row = pop_y_to_row((int16_t)Kid.y);
pop_leave_dir = 3;
return;
}
if (Kid.y >= 211) {
Kid.y = (uint8_t)(Kid.y - 189);
Kid.curr_row = pop_y_to_row((int16_t)Kid.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 = Kid.frame;
/* seg002 leave_room: комнату НЕ покидаем во время разворота (action turn),
* подъёма-с-зацепа (climb, кадры 135..149) и вставания из приседа
* (110..119). Без этого гарда разворот у правого шва (взгляд флипается
* право→лево, char_x_left проскакивает >=198) телепортирует Kid сквозь
* закрытые ворота в соседнюю комнату (leave_room возвращает 1). */
if (Kid.action == ACT_TURN ||
(f >= 135 && f < 150) ||
(f >= 110 && f < 120))
return;
/* Передний край — ТОЛЬКО через char_x_forward_edge (obj_x-арифметика
* set_char_collision): в зоне шва (<=57) он на пиксель правее чистого
* Kid.x+dx, и Kid, осевший у ворот на x=57, порога leave-left НЕ достигает
* (эталон SDLPoP: экран не переключается, BUG-SEAM-PINGPONG). */
fx = char_x_forward_edge();
if (Kid.direction >= 0) { /* взгляд вправо: fx = char_x_right */
uint8_t t9 = get_tile(9, Kid.curr_row);
if (g_link_r && fx >= 201 &&
t9 != TILE_DOORTOP_FLOOR && t9 != TILE_DOORTOP) {
/* leave_room (seg002): под doortop у правой кромки выхода нет */
Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2;
} else if (g_link_l && fx <= 57) {
Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1;
}
} else { /* взгляд влево: fx = char_x_left */
if (g_link_l && fx <= 54) {
Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1;
} else if (g_link_r && fx >= 198) {
Kid.x = (uint8_t)(Kid.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),
* выровнять по колонке, take_hp(100) = смерть, seq_51 (импейл). */
static void spiked(uint8_t tilepos)
{
pop_trob_modif(g_room)[tilepos] = 0xFF;
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
Kid.x = (uint8_t)(pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
Kid.x = (uint8_t)char_dx_forward(8);
Kid.fall_y = 0;
take_hp(100); /* (TODO: sound_48_spiked) */
kid_set_seq(SEQ_51_SPIKED);
pop_kid_play();
determine_col();
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 = Kid.frame, tp;
int h;
if (Kid.curr_col < 0 || Kid.curr_col > 9 || Kid.curr_row < 0 || Kid.curr_row > 2)
return;
if (get_tile_at_char() != TILE_SPIKE) return;
tp = (uint8_t)(Kid.curr_row * 10 + Kid.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 (Kid.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 = Kid.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;
}
}
}
void pop_phys_tick(void) __banked
{
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
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: уход вбок → переход в соседнюю комнату */
}
}