diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 241fd91..b4ff1d7 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -21,14 +21,28 @@ MEMORY ?= huge # small-раскладка + банки кода в W # горячих банк→банк переходов не создаёт (в отличие от pop_level, чей # pop_level_tile зовётся из банка 2 на КАЖДЫЙ тайл). Шина control_* # переехала в pop_state.c — см. там. -EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c $(PROF_FLAGS) +# --max-allocs-per-node: во сколько вариантов размещения регистров SDCC +# упирается на узел. У sprinter-cc дефолт 100000 (агрессивно, как fast-сборки +# библиотек) — на roomtest это МИНУТЫ на банк, потому что модули крупные. +# Дефолт SDCC — 3000, и для разработки его достаточно: разница в размере +# единицы процента, а сборка кратно быстрее. Замер (2026-08-05) — в шапке +# TASKS.md, запись BUILD-FAST. +# +# make — быстрая сборка (ALLOCS=3000) +# make ALLOCS=100000 — как раньше: минимальный код, для замеров размера +# и для «релизного» образа +# +# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том +# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. +ALLOCS ?= 3000 +EXTRA_FLAGS ?= --gfx 256 --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте # MAME = время фазы в растровых строках. PROF ?= 1 PROF_FLAGS := -DPROF_BORDER=$(PROF) -EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c +EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_level.c pop_redraw.c pop_geom.c pop_guard.c BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md index 801ce8f..ed72f78 100644 --- a/applications/PoP/roomtest/TASKS.md +++ b/applications/PoP/roomtest/TASKS.md @@ -44,6 +44,7 @@ | 4 | [L3-CHKP](#l3-chkp) | чекпойнт ур. 3 | корректный респавн ур. 3 | | — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | | — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 | +| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | | ✔ | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | **СДЕЛАНО 2026-08-05** | | ✔ | [MEM-BANK5](#mem-bank5) | `pop_ctrl.c` → банк 5 | **СДЕЛАНО 2026-08-05**: куча 180 Б → 2298 Б | @@ -197,6 +198,34 @@ loose-плиту (комната 7, колонка 4, ряд 0). Механик есть (сделана в L2), остаётся сам флаг и подмена старта. +### TUNE-1. Параметры движка — в конфиг, а не в код + +**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном +заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки +(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`). +Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а +не константой по месту. + +**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без +пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под +моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа, +комментарии `;`, неизвестные ключи игнорировать), файл необязателен — +нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`, +`[debug]`, `[enhancements]`. + +**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) + +`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его +(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и +опций — часть задачи: у него уже разложены по группам стартовые +HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало, +чекпойнт), тайминги ворот и пик, скорости, а отдельной группой — +`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже +портировали). Брать всё подряд не надо: переносим по мере того, как +константа реально понадобилась в игре. + +**Оговорка по памяти.** Парсер и таблица параметров — холодный код, +исполняется один раз при старте: кандидат в банк, а не в резидент W1. + ### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН** > **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 392d690..218d5a2 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -32,6 +32,7 @@ #include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */ #include "pop_level.h" /* pop_level_start_room — вход в дверь уровня */ #include "pop_guard.h" /* SWORD_2_DRAWN — ветка приземления с мечом */ +#include "pop_tune.h" /* POP_ENABLE_JUMP_GRAB и прочие настройки */ /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ #define TILE_EMPTY 0 @@ -69,6 +70,7 @@ #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 /* прыжок вверх и зацеп (пол сзади) */ @@ -628,6 +630,70 @@ static void check_grab(void) } } +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 = Kid.frame; + uint8_t grab_tile; + int8_t grab_col, grab_row; + + /* Сам выключатель проверяют ТОЧКИ ВЫЗОВА (иначе SDCC ругается на + * недостижимый код при POP_ENABLE_JUMP_GRAB = 1). */ + if (Kid.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)(Kid.curr_row - 1); /* СНЯТЬ до play_seq: он двигает ряд */ + + Kid.x = (uint8_t)(pop_x_bump[grab_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX); + Kid.x = (uint8_t)char_dx_forward(Kid.direction < 0 ? -12 : 2); + Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1]; + kid_set_seq(SEQ_9_GRAB_WHILE_JUMPING); + pop_kid_play(); + 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)(Kid.curr_row + 1); @@ -1093,6 +1159,11 @@ static void check_on_floor(void) static void check_action(void) { uint8_t action = Kid.action, frame = Kid.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, @@ -1378,6 +1449,13 @@ static void check_bumped_look_right(void) int coll_left; if (Kid.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; @@ -1393,6 +1471,10 @@ static void check_bumped_look_left(void) int coll_left; if (Kid.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; diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 9b7421f..590dbca 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -14,6 +14,7 @@ * нельзя звать из __banked-кода. */ #include "pop_bg.h" #include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */ +#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */ /* Коды тайлов (fg & 0x1F), которые анимируются. */ #define TILE_SPIKE 0x02 @@ -54,7 +55,7 @@ static pop_rnd_t trob_seed; #define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv)) -void pop_trob_reset(void) +void pop_trob_reset(void) __banked { uint8_t i; for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0; @@ -63,7 +64,7 @@ void pop_trob_reset(void) trobs_count = 0; } -uint8_t *pop_trob_modif(uint8_t room) +uint8_t *pop_trob_modif(uint8_t room) __banked { if (room < 1 || room > POP_NROOMS) return 0; if (!room_seen[room - 1]) { @@ -108,7 +109,7 @@ static int find_trob(uint8_t room, uint8_t tilepos) return -1; } -void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) +void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked { int f = find_trob(room, tilepos); if (f >= 0) { trobs[f].type = type; return; } @@ -119,7 +120,7 @@ void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) trobs_count++; } -void pop_trob_room_changed(void) +void pop_trob_room_changed(void) __banked { uint8_t i; for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0; @@ -130,7 +131,7 @@ void pop_trob_room_changed(void) * тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch, * seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька * (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */ -void pop_trob_anim_room(uint8_t room) +void pop_trob_anim_room(uint8_t room) __banked { uint8_t *mod = pop_trob_modif(room); uint8_t i, code; @@ -280,11 +281,14 @@ static void do_trigger_list(uint8_t index, uint8_t button_type) } void pop_trigger_button(uint8_t room, uint8_t tilepos, - uint8_t button_type, uint8_t modifier) + uint8_t button_type, uint8_t modifier) __banked { uint8_t link_timer = get_doorlink_timer(modifier); if (link_timer != 0x1F) { /* не заклинено */ - set_doorlink_timer(modifier, 5); + /* Оригинал ставит 5; вынесено в pop_tune.h, потому что проверять + * руками проход в закрывающуюся решётку при таком окне почти + * невозможно (см. BUG-GATE-PASS-1). */ + set_doorlink_timer(modifier, POP_DBG_GATE_HOLD); if (link_timer < 2) { pop_add_trob(room, tilepos, 1); /* кнопка нажимается */ /* seg007:766: щелчок кнопки — шум, страж оборачивается. Звука у @@ -362,7 +366,7 @@ static void animate_door(uint8_t *m, int8_t *type) *m = mod; } -uint8_t pop_trob_room_active(uint8_t room) +uint8_t pop_trob_room_active(uint8_t room) __banked { uint8_t i; for (i = 0; i < trobs_count; i++) @@ -387,7 +391,7 @@ static uint8_t torch_tick; #define TORCH_ANIM_NOW 1 #endif -void pop_process_trobs(uint8_t cur_room) +void pop_process_trobs(uint8_t cur_room) __banked { uint8_t i, w = 0; #if TORCH_ANIM_DIV > 1 diff --git a/applications/PoP/roomtest/pop_trob.h b/applications/PoP/roomtest/pop_trob.h index 5d08f11..f40e756 100644 --- a/applications/PoP/roomtest/pop_trob.h +++ b/applications/PoP/roomtest/pop_trob.h @@ -25,41 +25,41 @@ /* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и * очистить список trob. */ -void pop_trob_reset(void); +void pop_trob_reset(void) __banked; /* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт. * При ПЕРВОМ обращении лениво инициализируется из backtable уровня * (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне * диапазона → 0. */ -uint8_t *pop_trob_modif(uint8_t room); +uint8_t *pop_trob_modif(uint8_t room) __banked; /* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить * type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */ -void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type); +void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked; /* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать * modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице. * Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */ -void pop_process_trobs(uint8_t cur_room); +void pop_process_trobs(uint8_t cur_room) __banked; /* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку * LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button. * room/tilepos — кнопка; button_type — её тайл-код; modifier — её modif * (= индекс в LINKLOC). Звать из check_press (pop_map). */ void pop_trigger_button(uint8_t room, uint8_t tilepos, - uint8_t button_type, uint8_t modifier); + uint8_t button_type, uint8_t modifier) __banked; /* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через * шов (roomtest не дёргает шов, пока сосед не анимируется). */ -uint8_t pop_trob_room_active(uint8_t room); +uint8_t pop_trob_room_active(uint8_t room) __banked; /* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты * (trob-состояние в room_modif персистентно и НЕ трогается). Звать в * enter_room. */ -void pop_trob_room_changed(void); +void pop_trob_room_changed(void) __banked; /* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы, * зелья) со случайной стартовой фазой. Звать в enter_room. */ -void pop_trob_anim_room(uint8_t room); +void pop_trob_anim_room(uint8_t room) __banked; #endif diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h new file mode 100644 index 0000000..94ad713 --- /dev/null +++ b/applications/PoP/roomtest/pop_tune.h @@ -0,0 +1,66 @@ +/* + * pop_tune.h — НАСТРАИВАЕМЫЕ КОНСТАНТЫ движка (аналог custom_options + * SDLPoP) в одном месте. + * + * Зачем отдельный заголовок. У оригинала «особые события» уровней зашиты + * магическими числами прямо в код (`current_level == 3 && Char.room == 7`), + * и SDLPoP вынес их в структуру `custom_options_type` (types.h) с + * загрузкой из SDLPoP.ini и правкой через меню Settings/Mods. Нам нужно + * то же самое по двум причинам: + * + * 1. ОТЛАДКА. Некоторые сцены руками почти не проверить (успеть в + * закрывающуюся решётку), и единственный вменяемый способ — временно + * растянуть тайминг. Такие крутилки обязаны жить в одном месте, а не + * прятаться числами по трём модулям. + * 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа. + * + * Пока это только `#define` — ни INI, ни меню нет (см. TASKS.md, запись + * TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг + * заводится ЗДЕСЬ, а не константой по месту. Имена — как у SDLPoP + * (custom->имя), чтобы сверка с оригиналом оставалась механической. + */ +#ifndef POP_TUNE_H +#define POP_TUNE_H + +/* ---- Чекпойнт уровня 3 (SDLPoP: checkpoint_*) ----------------------- * + * Оригинал: level3_set_chkp (seg002:0665) взводит флаг, когда Кид уходит + * ВЛЕВО ИЗ комнаты 7 (вызов стоит до goto_other_room, поэтому Char.room там + * ещё старая); do_startpos (seg003:0141) подменяет по + * нему стартовую позицию и убирает loose-плиту, через которую туда + * попадают (иначе после респавна дорога назад была бы уже провалена). */ +#define POP_CHKP_LEVEL 3 /* checkpoint_level */ +#define POP_CHKP_ROOM 7 /* комната, ИЗ КОТОРОЙ уходят влево (у + * SDLPoP хардкод; это НЕ комната + * назначения — см. roomtest.c) */ +#define POP_CHKP_RESPAWN_ROOM 2 /* checkpoint_respawn_room */ +#define POP_CHKP_RESPAWN_TILEPOS 6 /* checkpoint_respawn_tilepos */ +#define POP_CHKP_RESPAWN_DIR (-1) /* checkpoint_respawn_dir: влево */ +#define POP_CHKP_CLEAR_ROOM 7 /* checkpoint_clear_tile_room */ +#define POP_CHKP_CLEAR_COL 4 /* checkpoint_clear_tile_col */ +#define POP_CHKP_CLEAR_ROW 0 /* checkpoint_clear_tile_row */ + +/* ---- Отладочные крутилки (в оригинале и в SDLPoP их НЕТ) ------------ * + * POP_DBG_GATE_HOLD — сколько кадров решётка держится поднятой после + * нажатия кнопки, ПРЕЖДЕ чем пойдёт вниз. В оригинале это таймер связи + * дверей: trigger_button ставит 5, animate_button вычитает по единице, и + * решётка стоит, пока таймер > 1 (то есть ~4 кадра после того, как Кид + * сошёл с кнопки). Проверять руками проход в закрывающуюся решётку при + * таком окне почти невозможно, поэтому значение вынесено сюда: поставь + * 30 — и успеешь дойти спокойно. ОБЯЗАН быть 5 в игровой сборке. + * + * ПОТОЛОК 30. Таймер связи — пять бит модификатора кнопки (маска 0x1F), а + * значение 31 у оригинала означает «событие заклинено» (`link_timer != 0x1F` + * в trigger_button). Больше 30 ставить нельзя — кнопка перестанет + * срабатывать вовсе. */ +#define POP_DBG_GATE_HOLD 5 + +/* ---- Улучшения SDLPoP (в ОРИГИНАЛЕ их нет) -------------------------- * + * POP_ENABLE_JUMP_GRAB — зацепиться за кромку можно ПРЯМО В ПРЫЖКЕ, если + * Shift зажат заранее (SDLPoP `enable_jump_grab`, seg006:1228). Ваниль + * разрешает зацеп только в начале падения (`check_grab`, кадры 102..105), + * то есть Shift приходится нажимать уже в полёте — это и раздражало на + * приёмке. 0 — как в оригинале, 1 — как в SDLPoP с включёнными + * enhancements. */ +#define POP_ENABLE_JUMP_GRAB 1 + +#endif /* POP_TUNE_H */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index c56a037..bee8669 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -19,6 +19,7 @@ #include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */ #include "pop_guard.h" #include "pop_cheat.h" +#include "pop_tune.h" __sfr __at (0xFE) io_border; @@ -60,7 +61,7 @@ __sfr __at (0xFE) io_border; * останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку * runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра * доходила до конца загрузки ресурсов и вставала намертво в DSS. */ -const uint8_t n_banks = 5; +const uint8_t n_banks = 6; /* Режим читов (pop_cheat.h): на время разработки включаем в main. */ uint8_t pop_cheats; @@ -369,6 +370,12 @@ static void find_start_level_door(uint8_t room) } } +/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7 + * уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет + * стартовую позицию. Сбрасывается началом игры и прохождением уровня — + * ровно как в оригинале (seg003:37 и seg003:413). */ +static uint8_t pop_checkpoint; + static void pop_start_level(void) { uint8_t entry_pose = pop_level_entry_pose(pop_current_level); @@ -376,6 +383,16 @@ static void pop_start_level(void) uint8_t pos = pop_level_start_pos(); int8_t dir = (int8_t)~pop_level_start_dir(); uint8_t seq = SEQ_TURN; + /* Особое событие: старт с чекпойнта (do_startpos, seg003:0141). + * Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles() + * ниже, иначе рестарт её вернёт. */ + uint8_t chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint); + if (chkp) { + start_room = POP_CHKP_RESPAWN_ROOM; + pos = POP_CHKP_RESPAWN_TILEPOS; + dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную + * и разворачивается seq_5 */ + } /* Рестарт уровня = load_level() заново (play_level, seg003:57): в * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы — @@ -386,6 +403,12 @@ static void pop_start_level(void) pop_level_reset_guards(); pop_guard_reset(); /* start_level: Guard.direction = none */ pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ + /* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в + * комнату-чекпойнт после респавна остался бы непройденным. */ + if (chkp) + pop_level_set_tile(POP_CHKP_CLEAR_ROOM, + (uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL), + TILE_EMPTY); /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы @@ -450,6 +473,7 @@ int main(void) } pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */ pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */ + pop_checkpoint = 0; /* init_game (seg003:37) */ initgraph(); gfx_set_visible_page(0); @@ -618,7 +642,25 @@ int main(void) } } else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */ uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1)); + uint8_t left = (uint8_t)(pop_leave_dir == 1); pop_leave_dir = 0; + /* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал + * зовёт его из leave_room ДО goto_other_room, то есть Char.room + * там ещё СТАРАЯ. Значит условие `Char.room == 7` читается как + * «Кид УХОДИТ ИЗ комнаты 7 влево» (в комнату 2), а не «входит в + * неё». Смысл: чекпойнт даётся за то, что комната 7 пройдена + * НАСКВОЗЬ, и респавн ставится туда, куда Кид только что попал. + * + * Сначала я поставил проверку на комнату НАЗНАЧЕНИЯ — и это + * поймал пользователь прогоном в SDLPoP: он умер В комнате 7, но + * влево из неё не выходил, и оригинал вернул его в стартовую + * комнату 9, а плита осталась цела. Заодно тут запоминается + * достигнутый потолок HP: смерть после чекпойнта его не отбирает. */ + if (left && pop_current_level == POP_CHKP_LEVEL && + cur_room == POP_CHKP_ROOM) { + pop_checkpoint = 1; + hitp_beg_lev = hitp_max; + } if (nb) { enter_room(nb); gfx_set_draw_page(back); @@ -659,6 +701,7 @@ int main(void) * поднятая на уровне «жизнь» (большая склянка) переезжает * дальше, а урон — нет (do_startpos дольёт до потолка). */ hitp_beg_lev = hitp_max; + pop_checkpoint = 0; /* уровень пройден (seg003:413) */ if (pop_level_load_num(pop_next_level) != 0) { pop_next_level = pop_current_level; /* файла нет — остаёмся */ } else { diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index 2c37846..4a40b86 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -130,7 +130,7 @@ void tk_key_down(uint16_t code) uint8_t tk_modif[30]; uint8_t tk_start_room; -uint8_t *pop_trob_modif(uint8_t room) { (void)room; return tk_modif; } +uint8_t *pop_trob_modif(uint8_t room) __banked { (void)room; return tk_modif; } uint8_t pop_level_start_room(void) { return tk_start_room; } /* Журнал: что физика попросила у соседей. Тест сверяет ФАКТ и параметры @@ -157,11 +157,11 @@ uint8_t tk_log_count(uint8_t kind) return c; } -void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) +void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked { log_put(TK_TROB, room, tilepos, (uint8_t)type); } void pop_trigger_button(uint8_t room, uint8_t tilepos, - uint8_t button_type, uint8_t modifier) + uint8_t button_type, uint8_t modifier) __banked { (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); } void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)