diff --git a/applications/PoP/docs/levels_plan.md b/applications/PoP/docs/levels_plan.md index bafc12a..9b636e5 100644 --- a/applications/PoP/docs/levels_plan.md +++ b/applications/PoP/docs/levels_plan.md @@ -65,7 +65,16 @@ up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`, --- -## 2. Шаг 1 — машинерия перехода (цель: уровни 1 → 2 → 3) +## 2. Шаг 1 — машинерия перехода (цель: уровни 1 → 2 → 3) — **СДЕЛАН 2026-08-04** + +> **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 → +> Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие +> колонны рисуются) → уровень 3. Разбор что именно сделано и что по +> уровню 2 осталось — `../roomtest/TASKS.md`, запись **L2**. +> +> Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется: +> **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина +> двери уровня) и **большую склянку** `add_life` (тип зелья 2, комната 20). Порядок именно такой; каждый пункт проверяем в MAME отдельно. diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index f46d999..ad9b47e 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -44,7 +44,10 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`). Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр); **1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии); -**ESC** — выход. +**ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I** — +бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень, +**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру +(`pop_level_load_num`), на диске лежат все 15. ## Модули diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 3157683..6b8a95e 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -26,7 +26,11 @@ EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_ctrl.c pop_level.c pop_trob. BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS -LVL_DATA := $(LVL_DIR)/res2001.bin +# ВСЕ уровни игры (res2001..res2015, по 2305 Б = 34 КБ на всех): переход +# уровень→уровень и чит Shift+L требуют, чтобы файл следующего лежал на +# диске. res2000 — демо-уровень, в скоуп не входит. +LVL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 +LVL_DATA := $(foreach n,$(LVL_NUMS),$(LVL_DIR)/res20$(n).bin) BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \ $(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \ $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \ @@ -58,7 +62,7 @@ hdd: $(EXAMPLE).exe $(foreach f,$(BG_DATA),BG:$(f)) \ $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \ - LEVELS:$(LVL_DATA) + $(foreach f,$(LVL_DATA),LEVELS:$(f)) TC := $(PROJ_ROOT)/applications/PoP/toolchain $(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md index a6aa8c2..eca580f 100644 --- a/applications/PoP/roomtest/TASKS.md +++ b/applications/PoP/roomtest/TASKS.md @@ -444,6 +444,88 @@ BUG-1 (провал на row 1 при переходе через открыты вверх, шов при анимации решётки, грани дальней колонны) и подъём на тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md). +### L2. Переход на уровень 2 и его игра — **МАШИНЕРИЯ СДЕЛАНА 2026-08-04** + +Порт `levels_plan.md` §2 (шаг 1). Что появилось: + +- **Номер уровня стал состоянием.** `pop_current_level` (порт + `current_level`) в `pop_level.c`; `pop_next_level` больше не флаг, а + НОМЕР — `END_LEVEL` его инкрементит (как `++next_level`, seg006:662), а + главный цикл срабатывает по расхождению `pop_next_level != + pop_current_level` (порт `play_level_2`, seg003:0386). +- **Загрузка по номеру** — `pop_level_load_num(n)`: `LEVELS\res20NN.bin` + (fallback `a:\`), старая EMM-страница отпускается ТОЛЬКО после успешной + загрузки новой (нет файла — играем дальше на текущем). На диск кладутся + все 15 уровней (34 КБ). +- **Потабличные различия** (`data.h:840..848`) — таблицы по 16 в + `pop_level.c`: `tbl_entry_pose` (поза входа), `tbl_guard_hp` (HP стража, + ушло из хардкода `3`), `tbl_guard_type` (**−1 = стражей нет**, иначе на + 14/15 они полезли бы из данных комнат), `tbl_level_type` (тайлсет — + пока только читается). +- **`find_start_level_door`** (seg003:02E6) — на уровне 2 это НЕ косметика: + стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина двери + уровня, и без `modif = 43` + `add_trob(...,3)` Кид материализуется внутри + глухой створки. Тип 3 = «быстро закрыть»: дверь захлопывается за спиной + за три кадра, как в оригинале. +- **HP через уровень** — `hitp_beg_lev` (seg003): рестарт уровня + откатывает HP к нему, пройденный уровень подтягивает его к `hitp_max`. + Заодно реализована **большая склянка** (`add_life`, тип зелья 2: +1 к + ПОТОЛКУ HP до 10) — на уровне 2 она есть, комната 20. +- **Меч** — `have_sword = level >= 2` (play_level, seg003:106), а не + жёсткий ноль. +- **Чит Shift+L** (seg000:698) — `pop_next_level = pop_current_level + 1`. + NB: `L` без Shift занят отладочным «осторожным шагом вправо» + (`pop_ctrl` `KBD_DBG_STEPR`); с Shift шаг тоже пройдёт, но уровень тут же + сменится — на практике не мешает. + +**Проверено в MAME (2026-08-04):** старт уровня 1 → Shift+L → уровень 2 +рисуется правильно (комната 5, большие колонны — новые для нас тайлы 8/9 — +на месте, дверь захлопнута, HP 3) → влево в комнату 4, страж на месте → +Shift+L → уровень 3 (комната 9). То есть цепочка загрузок работает +повторно, а не только один раз. + +**Что осталось по уровню 2 (не машинерия, а контент):** +- сквозное прохождение уровня 2 руками — приёмка, как L1-PASS; +- выход через дверь уровня 2 (комната 23) вживую: путь тот же, что уже + закрыт на уровне 1 (L1-EXIT), но не прогнан на этом уровне; +- цвет стража из данных (`guards_color`, на уровне 2 их три разных) — у нас + один атлас; отдельная задача, к переходам отношения не имеет. + +### DBG-CHEATS. Отладочные читы SDLPoP — разведка сделана, код не написан + +Записано 2026-08-03 (по ходу охоты за BUG-GATE-PASS-1, отложено на потом). +Мотив прямой: мост MAME **теряет нажатия при быстрой отправке**, поэтому +подогнать Кида в нужную позу для отладки автоматикой сейчас нельзя — именно +на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1. + +**Берём (дёшево и бьёт в наш класс багов):** + +- **`[` / `]` — сдвинуть Кида на пиксель влево/вправо.** Оригинал — + `../SDLPoP/src/seg000.c:1828`: + ```c + if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x; + else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x; + ``` + У нас: коды PS/2 set 2 `[` = **0x54**, `]` = **0x5B** в `pop_cheat.h` + рядом с `KBD_CHEAT_KILL/IMMO/SWORD`; обработка — в том же блоке читов + `roomtest.c` (~строка 479), **по фронту** (`*_prev`, как у остальных), + иначе одно нажатие уедет на десяток пикселей. Работает по `Kid.x` + напрямую: геометрию персонажа в этом порте меняет не диспетчер (см. + грабли L1-EXIT). + Первый же потребитель — BUG-GATE-PASS-1: там надо снять `Kid.x` и + `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё стоит + на её тайле. +- **Shift+L — следующий уровень.** Уже входит в L2-машинерию + (`../docs/levels_plan.md` §2.4): `++pop_next_level` и всё. + +**Остальные — оценка, а не обязательство:** + +| Чит | Вердикт | +|-----|---------| +| **T — таймер** | **не сейчас**: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой | +| **F — остаток feather-fall** | **вместе с Shift+W**: ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно | +| **Shift+F9 — quickload с тем же уровнем** | **самое ценное и самое дорогое**: это сериализация `Char` + `room_modif` всех комнат + trob'ов + стражей (`levels_plan.md` §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки. Кандидат сразу после того, как заработают уровни | + --- ## Отложено осознанно (не брать, пока не появится причина) diff --git a/applications/PoP/roomtest/pop_cheat.h b/applications/PoP/roomtest/pop_cheat.h index 22e6637..8589a6d 100644 --- a/applications/PoP/roomtest/pop_cheat.h +++ b/applications/PoP/roomtest/pop_cheat.h @@ -19,5 +19,9 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ #define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */ #define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */ #define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */ +#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */ + +/* Последний уровень игры (tbl_* в pop_level рассчитаны на 0..15). */ +#define POP_LAST_LEVEL 15 #endif diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index 76d896e..111133c 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -129,6 +129,10 @@ void pop_guard_enter(uint8_t room) uint8_t tile, color, skill; int8_t dir; pop_guard_reset(); + /* load_lev_spr (seg000:1112): tbl_guard_type == −1 — стражей на уровне + * НЕТ вообще (14/15), их графика даже не грузится. Без этой проверки + * на таком уровне полезли бы стражи из данных комнат. */ + if (pop_level_guard_type(pop_current_level) < 0) return; if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return; /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из * колонки тайла, а не берётся из файла. */ @@ -147,8 +151,9 @@ void pop_guard_enter(uint8_t room) * всё, что вне диапазона, оригинал сводит к 3. */ guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3); /* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень]. - * Уровень 1 -> 3; extrastrength ненулевой только у skill 4. */ - guardhp_max = guardhp_curr = (uint8_t)(3 + (guard_skill == 4 ? 1 : 0)); + * extrastrength (seg002:38) ненулевой только у skill 4 (=1). */ + guardhp_max = guardhp_curr = + (uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0)); guardhp_delta = 0; /* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у * ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c index 05f243b..0b817b9 100644 --- a/applications/PoP/roomtest/pop_level.c +++ b/applications/PoP/roomtest/pop_level.c @@ -98,6 +98,52 @@ void pop_level_free(void) if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; } } +/* ---- Номер уровня и потабличные различия ---------------------------- */ + +uint8_t pop_current_level; + +/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = демо). + * Держим целиком по 16 байт: короче и дешевле любых «оптимизаций», а + * расхождение с оригиналом сразу видно глазами. */ +static const uint8_t tbl_entry_pose[16] = + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0 }; +static const uint8_t tbl_guard_hp[16] = + { 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0, 0 }; +static const int8_t tbl_guard_type[16] = + { 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1, -1 }; +static const uint8_t tbl_level_type[16] = + { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0 }; + +uint8_t pop_level_entry_pose(uint8_t n) { return n < 16 ? tbl_entry_pose[n] : 0; } +uint8_t pop_level_guard_hp(uint8_t n) { return n < 16 ? tbl_guard_hp[n] : 3; } +int8_t pop_level_guard_type(uint8_t n) { return n < 16 ? tbl_guard_type[n] : -1; } +uint8_t pop_level_tileset(uint8_t n) { return n < 16 ? tbl_level_type[n] : 0; } + +int pop_level_load_num(uint8_t n) +{ + /* Имена патчим на месте, а не через sprintf: printf-семейство тянет в + * резидент W1 форматтер, которого здесь больше негде применить. */ + static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */ + static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */ + uint8_t saved_blk = lvl_blk, saved_page = lvl_page, saved_ok = lvl_ok; + + if (n > 15) return -1; + pri[12] = alt[8] = (char)('0' + n / 10); + pri[13] = alt[9] = (char)('0' + n % 10); + + /* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой: + * pop_level_load сам аллоцирует страницу, и если файла нет — остаёмся + * на текущем уровне вместо падения в пустой уровень. */ + lvl_ok = 0; /* чтобы pop_level_load не мешался */ + if (pop_level_load(pri) != 0 && pop_level_load(alt) != 0) { + lvl_blk = saved_blk; lvl_page = saved_page; lvl_ok = saved_ok; + return -1; + } + if (saved_ok) mem_free_block(saved_blk); + pop_current_level = n; + return 0; +} + /* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже * замаплена вызывающим). */ static const uint8_t *room_fg_ptr(uint8_t r) diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index ace23fa..0838570 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -26,6 +26,36 @@ int pop_level_load(const char *path); void pop_level_free(void); +/* ---- Номер уровня и потабличные различия (levels_plan.md §1) -------- */ + +/* Порт current_level (seg000 load_lev_spr: current_level = next_level = + * level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают + * потабличные функции ниже, стражи (HP/тип) и стартовая логика. */ +extern uint8_t pop_current_level; + +/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять + * `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить + * pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда + * pop_current_level и страница НЕ тронуты — играем дальше на старом). + * Порт связки load_lev_spr + load_level (seg000:1098/1169). */ +int pop_level_load_num(uint8_t n); + +/* tbl_entry_pose (data.h:848): 1 — падение внутрь (ур. 1), 2 — вбегание + * (ур. 13), 0 — разворот на месте. Индекс — номер уровня. */ +uint8_t pop_level_entry_pose(uint8_t n); + +/* tbl_guard_hp (data.h:846) — база HP стража на уровне (get_guard_hp, + * seg002:0044: extrastrength[skill] + tbl_guard_hp[level]). */ +uint8_t pop_level_guard_hp(uint8_t n); + +/* tbl_guard_type (data.h:844): 0 обычный, 1 толстый, 2 скелет, 3 визирь, + * 4 тень, −1 — стражей на уровне НЕТ (уровни 14/15). */ +int8_t pop_level_guard_type(uint8_t n); + +/* tbl_level_type (data.h:840): 0 = dungeon, 1 = palace. Пока читается + * только для контроля — второй тайлсет это levels_plan.md §3. */ +uint8_t pop_level_tileset(uint8_t n); + /* Извлечь комнату room (1..24) в массивы приложения (W2): * fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw); * lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 099debc..9e88fc0 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -117,6 +117,14 @@ uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */ uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */ +/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень. + * Смерть/рестарт уровня возвращают ровно его (do_startpos: hitp_max = + * hitp_curr = hitp_beg_lev), а по ПРОХОЖДЕНИИ уровня он подтягивается к + * достигнутому максимуму (hitp_beg_lev = hitp_max) — так «жизнь» из большой + * склянки переносится на следующий уровень, а из мёртвых прогонов — нет. + * Стартовое значение (custom->start_hitp = 3) ставит main через + * pop_kid_hp_newgame — инициализатором нельзя: модуль банкованный. */ +uint8_t hitp_beg_lev; /* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки * (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале. * Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и @@ -162,7 +170,27 @@ void pop_map_set(uint8_t *fg) __banked { g_fg = fg; } void pop_map_set_room(uint8_t room) __banked { g_room = room; } -void pop_kid_hp_reset(void) __banked { hitp_curr = hitp_max = 3; hitp_delta = 0; pop_kid_dead = 0; } +/* do_startpos (seg003:0160): HP уровня = hitp_beg_lev, а не константа — + * иначе большая склянка теряется на границе уровня. */ +void pop_kid_hp_reset(void) __banked +{ + hitp_curr = hitp_max = hitp_beg_lev; + hitp_delta = 0; + pop_kid_dead = 0; +} + +/* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */ +void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; } + +/* add_life (seg000:11EC) + set_health_life (seg000:1200): большая склянка + * поднимает ПОТОЛОК HP (до max_hitp_allowed = 10) и доливает до него. */ +static void pop_kid_add_life(void) +{ + uint8_t hpmax = (uint8_t)(hitp_max + 1); + if (hpmax > POP_MAX_HITP) hpmax = POP_MAX_HITP; + hitp_max = hpmax; + hitp_delta = (int8_t)(hitp_max - hitp_curr); +} /* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */ static uint8_t take_hp(uint8_t count) @@ -906,13 +934,18 @@ void pop_proc_get_object(void) __banked pop_flash_time = 2; } break; + case 2: /* большая склянка: +1 к ПОТОЛКУ HP */ + pop_kid_add_life(); + pop_flash_red = 1; + pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */ + break; case 5: /* «злое» зелье: −1 HP */ hitp_delta = -1; pop_flash_red = 1; pop_flash_time = 2; break; - default: /* 2 жизнь / 3 перо / 4 переворот / - * 6 открыть — TODO с их уровнями */ + default: /* 3 перо / 4 переворот / 6 открыть — + * TODO вместе с их уровнями (7/9) */ break; } } diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 0d705ff..5831fd0 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -44,6 +44,15 @@ extern uint8_t hitp_max; /* стартовое HP (для индикатора extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */ void pop_kid_hp_reset(void) __banked; +/* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт + * уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень + * подтягивает его к hitp_max (play_level_2, seg003:410) — так «жизнь» из + * большой склянки переезжает на следующий уровень. Правит главный цикл. */ +extern uint8_t hitp_beg_lev; +#define POP_START_HITP 3 /* custom->start_hitp */ +#define POP_MAX_HITP 10 /* custom->max_hitp_allowed */ +void pop_kid_hp_newgame(void) __banked; /* начало ИГРЫ: hitp_beg_lev = 3 */ + /* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object). * pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или * tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 786110b..62d928e 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -90,10 +90,7 @@ static void kbd_idle(void) #define ROOM_1 1 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ -/* Поза входа на уровне (tbl_entry_pose, data.h:848): у уровня 1 — падение - * внутрь плюс нажатие кнопки. Уровень пока один, поэтому константа; когда - * появятся уровни 2+ (levels_plan.md), это станет таблицей в pop_level. */ -#define LEVEL_ENTRY_POSE ENTRY_POSE_FALL +#define FIRST_LEVEL 1 /* с какого уровня начинается игра */ #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ @@ -337,16 +334,38 @@ static void enter_room(uint8_t room) * это оно захлопывает решётку за спиной в первой комнате; * 2 (уровень 13) — вбегание (seq_84_run); * иначе — разворот на месте (seq_5_turn). - * Таблица индексируется НОМЕРОМ УРОВНЯ, поэтому появится в pop_level, когда - * дойдём до уровней 2+ (levels_plan.md); пока уровень один. */ + * Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */ #define SEQ_TURN 5 #define SEQ_FALL 7 #define SEQ_RUN 84 #define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */ #define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */ +#define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */ -static void pop_start_level(uint8_t entry_pose) +/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ — + * это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью + * (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72). + * Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки: + * его стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина + * той самой двери. */ +static void find_start_level_door(uint8_t room) { + uint8_t *m = pop_trob_modif(room); + uint8_t tp; + if (!m) return; + /* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз + * за уровень, а pop_add_trob сам лезет в W0. */ + for (tp = 0; tp < 30; tp++) { + if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue; + m[tp] = 43; /* створка поднята полностью */ + pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */ + } +} + +static void pop_start_level(void) +{ + uint8_t entry_pose = pop_level_entry_pose(pop_current_level); + uint8_t start_room = pop_level_start_room(); uint8_t pos = pop_level_start_pos(); int8_t dir = (int8_t)~pop_level_start_dir(); uint8_t seq = SEQ_TURN; @@ -375,10 +394,17 @@ static void pop_start_level(uint8_t entry_pose) seq = SEQ_RUN; } - enter_room(pop_level_start_room()); + /* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть + * открыта уже в первом кадре (в оригинале find_start_level_door стоит + * между do_startpos и draw_level_first). */ + find_start_level_door(start_room); + + enter_room(start_room); kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir); Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ - pop_have_sword = 0; /* start_level: have_sword с уровня 2 */ + /* play_level (seg003:106): have_sword = level >= have_sword_from_level + * (=2) — начиная со второго уровня меч у Кида уже есть. */ + pop_have_sword = (uint8_t)(pop_current_level >= 2); pop_kid_hp_reset(); } @@ -392,7 +418,7 @@ int main(void) #ifdef ROOMNAV uint8_t nav_prev = 0; #endif - uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0; + uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0; if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */ puts("pop_bg_load failed"); @@ -410,11 +436,12 @@ int main(void) puts("kid atlas load failed"); return 1; } - if (pop_level_load("LEVELS\\res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */ - pop_level_load("a:\\res2001.bin") != 0) { + if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */ puts("level load failed"); return 1; } + pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */ + pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */ initgraph(); gfx_set_visible_page(0); @@ -431,7 +458,7 @@ int main(void) /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см. * pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места, * Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */ - pop_start_level(LEVEL_ENTRY_POSE); + pop_start_level(); gfx_set_draw_page(0); kid_draw(); pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), @@ -472,16 +499,27 @@ int main(void) * I — бессмертие Кида (наш отладочный, в оригинале его нет: * иначе боёвку проверять получается ровно три раза); * S — выдать меч, не бегая за ним в комнату 15. Штатно меч - * берётся с пола (pop_have_sword ставит подбор). */ + * берётся с пола (pop_have_sword ставит подбор); + * Shift+L — следующий уровень (seg000:698, ветка next_level = + * current_level + 1). Единственный вменяемый способ + * тестировать уровни 2..15. NB: L без Shift — отладочный + * «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с + * Shift шаг тоже пройдёт, но уровень тут же сменится, так + * что конфликта на практике нет. */ uint8_t k = kbd_raw_down(KBD_CHEAT_KILL); uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO); uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD); + uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) && + (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))); if (pop_cheats && k && !cheat_prev) pop_guard_kill(); if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal; if (pop_cheats && s && !sword_prev) pop_have_sword = 1; + if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL) + pop_next_level = (uint8_t)(pop_current_level + 1); cheat_prev = k; immo_prev = i; sword_prev = s; + nlvl_prev = nl; } if (frozen) { PROF(0); /* чёрный: ждём vsync */ @@ -545,7 +583,7 @@ int main(void) enter_room(down); /* координаты Kid уже репроецированы pop_map */ gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */ } else { /* выпал из уровня (нет комнаты снизу) — рестарт */ - pop_start_level(LEVEL_ENTRY_POSE); + pop_start_level(); gfx_set_draw_page(back); } } else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */ @@ -581,15 +619,25 @@ int main(void) } } } - if (pop_next_level) { /* END_LEVEL: доиграл seq_70 в двери уровня */ - /* Порт play_level_2 (seg003:0386): цикл уровня кончается, когда - * next_level разошёлся с current_level, и start_game грузит - * следующий. Уровень у нас пока один, поэтому «следующий» = тот - * же самый — зато вся цепочка (дверь → seq_70 → END_LEVEL → - * конец уровня) работает целиком и проверяема. Загрузка уровня - * 2 подключается ровно сюда, см. ../docs/levels_plan.md §2.2. */ - pop_next_level = 0; - pop_start_level(LEVEL_ENTRY_POSE); + if (pop_next_level != pop_current_level) { + /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): + * цикл уровня кончается, когда next_level разошёлся с + * current_level (END_LEVEL после seq_70 в двери уровня либо чит + * Shift+L), и play_level грузит следующий. + * + * HP переносится: hitp_beg_lev = hitp_max ДО загрузки — так + * поднятая на уровне «жизнь» (большая склянка) переезжает + * дальше, а урон — нет (do_startpos дольёт до потолка). */ + hitp_beg_lev = hitp_max; + if (pop_level_load_num(pop_next_level) != 0) { + pop_next_level = pop_current_level; /* файла нет — остаёмся */ + } else { + /* Всё состояние прошлого уровня сносит сам pop_start_level: + * pop_trob_reset обнуляет room_seen (модификаторы комнат + * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают + * из неё же, loose чистит enter_room. */ + pop_start_level(); + } gfx_set_draw_page(back); } if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ @@ -597,7 +645,7 @@ int main(void) if (dead_frames > RESPAWN_SETTLE && (kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) { dead_frames = 0; - pop_start_level(LEVEL_ENTRY_POSE); + pop_start_level(); gfx_set_draw_page(back); } }