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