L2: переход между уровнями и игра уровня 2

Порт levels_plan.md §2 (шаг 1) — машинерия смены уровня целиком.

- Номер уровня стал состоянием: pop_current_level (порт current_level),
  pop_next_level больше не флаг, а НОМЕР; главный цикл срабатывает по
  расхождению next != current (порт 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 = стражей на уровне нет), tbl_level_type.
- find_start_level_door (seg003:02E6): на уровне 2 стартовый тайл —
  правая половина двери уровня, без 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).

Проверено в MAME: уровень 1 → Shift+L → уровень 2 (комната 5, новые для
нас тайлы 8/9 «большая колонна» на месте, дверь захлопнута, HP 3) →
влево в комнату 4 со стражем → Shift+L → уровень 3.  Вход в стартовую
дверь уровня 2 по Up не срабатывает — как и должно (start_room-гард).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Александр Петров
2026-08-04 11:35:56 +03:00
parent 7ef007757b
commit e36828ae6e
11 changed files with 307 additions and 34 deletions
+10 -1
View File
@@ -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 отдельно. Порядок именно такой; каждый пункт проверяем в MAME отдельно.
+4 -1
View File
@@ -44,7 +44,10 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр); дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии); **1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
**ESC** — выход. **ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I**
бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень,
**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру
(`pop_level_load_num`), на диске лежат все 15.
## Модули ## Модули
+6 -2
View File
@@ -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 BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid KID_DIR := $(CURDIR)/../poc/res/kid
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS 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_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_env3.atl $(BG_DIR)/pop_env4.atl \
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.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,$(BG_DATA),BG:$(f)) \
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \
LEVELS:$(LVL_DATA) $(foreach f,$(LVL_DATA),LEVELS:$(f))
TC := $(PROJ_ROOT)/applications/PoP/toolchain TC := $(PROJ_ROOT)/applications/PoP/toolchain
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py $(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
+82
View File
@@ -444,6 +444,88 @@ BUG-1 (провал на row 1 при переходе через открыты
вверх, шов при анимации решётки, грани дальней колонны) и подъём на вверх, шов при анимации решётки, грани дальней колонны) и подъём на
тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md). тайл-кнопку: см. оговорку к 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). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки. Кандидат сразу после того, как заработают уровни |
--- ---
## Отложено осознанно (не брать, пока не появится причина) ## Отложено осознанно (не брать, пока не появится причина)
+4
View File
@@ -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_KILL 0x42 /* K (PS/2 set 2) — kill guard */
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */ #define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */ #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 #endif
+7 -2
View File
@@ -129,6 +129,10 @@ void pop_guard_enter(uint8_t room)
uint8_t tile, color, skill; uint8_t tile, color, skill;
int8_t dir; int8_t dir;
pop_guard_reset(); 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; if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
* колонки тайла, а не берётся из файла. */ * колонки тайла, а не берётся из файла. */
@@ -147,8 +151,9 @@ void pop_guard_enter(uint8_t room)
* всё, что вне диапазона, оригинал сводит к 3. */ * всё, что вне диапазона, оригинал сводит к 3. */
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3); guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень]. /* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
* Уровень 1 -> 3; extrastrength ненулевой только у skill 4. */ * extrastrength (seg002:38) ненулевой только у skill 4 (=1). */
guardhp_max = guardhp_curr = (uint8_t)(3 + (guard_skill == 4 ? 1 : 0)); guardhp_max = guardhp_curr =
(uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0));
guardhp_delta = 0; guardhp_delta = 0;
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у /* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит * ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
+46
View File
@@ -98,6 +98,52 @@ void pop_level_free(void)
if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; } 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-пространстве (страница уже /* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
* замаплена вызывающим). */ * замаплена вызывающим). */
static const uint8_t *room_fg_ptr(uint8_t r) static const uint8_t *room_fg_ptr(uint8_t r)
+30
View File
@@ -26,6 +26,36 @@
int pop_level_load(const char *path); int pop_level_load(const char *path);
void pop_level_free(void); 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): /* Извлечь комнату room (1..24) в массивы приложения (W2):
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw); * fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
* lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА * lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
+36 -3
View File
@@ -117,6 +117,14 @@ uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */ uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */ 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). Её ставит СВОРД-путь боёвки /* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки
* (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале. * (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале.
* Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и * Путь пик/падения (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_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). */ /* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
static uint8_t take_hp(uint8_t count) static uint8_t take_hp(uint8_t count)
@@ -906,13 +934,18 @@ void pop_proc_get_object(void) __banked
pop_flash_time = 2; pop_flash_time = 2;
} }
break; break;
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
pop_kid_add_life();
pop_flash_red = 1;
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
break;
case 5: /* «злое» зелье: −1 HP */ case 5: /* «злое» зелье: −1 HP */
hitp_delta = -1; hitp_delta = -1;
pop_flash_red = 1; pop_flash_red = 1;
pop_flash_time = 2; pop_flash_time = 2;
break; break;
default: /* 2 жизнь / 3 перо / 4 переворот / default: /* 3 перо / 4 переворот / 6 открыть —
* 6 открыть — TODO с их уровнями */ * TODO вместе с их уровнями (7/9) */
break; break;
} }
} }
+9
View File
@@ -44,6 +44,15 @@ extern uint8_t hitp_max; /* стартовое HP (для индикатора
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */ extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
void pop_kid_hp_reset(void) __banked; 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). /* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или * pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
* tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл * tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл
+73 -25
View File
@@ -90,10 +90,7 @@ static void kbd_idle(void)
#define ROOM_1 1 #define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
/* Поза входа на уровне (tbl_entry_pose, data.h:848): у уровня 1 — падение #define FIRST_LEVEL 1 /* с какого уровня начинается игра */
* внутрь плюс нажатие кнопки. Уровень пока один, поэтому константа; когда
* появятся уровни 2+ (levels_plan.md), это станет таблицей в pop_level. */
#define LEVEL_ENTRY_POSE ENTRY_POSE_FALL
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
@@ -337,16 +334,38 @@ static void enter_room(uint8_t room)
* это оно захлопывает решётку за спиной в первой комнате; * это оно захлопывает решётку за спиной в первой комнате;
* 2 (уровень 13) — вбегание (seq_84_run); * 2 (уровень 13) — вбегание (seq_84_run);
* иначе — разворот на месте (seq_5_turn). * иначе — разворот на месте (seq_5_turn).
* Таблица индексируется НОМЕРОМ УРОВНЯ, поэтому появится в pop_level, когда * Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */
* дойдём до уровней 2+ (levels_plan.md); пока уровень один. */
#define SEQ_TURN 5 #define SEQ_TURN 5
#define SEQ_FALL 7 #define SEQ_FALL 7
#define SEQ_RUN 84 #define SEQ_RUN 84
#define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */ #define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */
#define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */ #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(); uint8_t pos = pop_level_start_pos();
int8_t dir = (int8_t)~pop_level_start_dir(); int8_t dir = (int8_t)~pop_level_start_dir();
uint8_t seq = SEQ_TURN; uint8_t seq = SEQ_TURN;
@@ -375,10 +394,17 @@ static void pop_start_level(uint8_t entry_pose)
seq = SEQ_RUN; 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_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir);
Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ 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(); pop_kid_hp_reset();
} }
@@ -392,7 +418,7 @@ int main(void)
#ifdef ROOMNAV #ifdef ROOMNAV
uint8_t nav_prev = 0; uint8_t nav_prev = 0;
#endif #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) */ if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
puts("pop_bg_load failed"); puts("pop_bg_load failed");
@@ -410,11 +436,12 @@ int main(void)
puts("kid atlas load failed"); puts("kid atlas load failed");
return 1; return 1;
} }
if (pop_level_load("LEVELS\\res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */ if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */
pop_level_load("a:\\res2001.bin") != 0) {
puts("level load failed"); puts("level load failed");
return 1; 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(); initgraph();
gfx_set_visible_page(0); gfx_set_visible_page(0);
@@ -431,7 +458,7 @@ int main(void)
/* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см. /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
* pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места, * pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места,
* Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */ * Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */
pop_start_level(LEVEL_ENTRY_POSE); pop_start_level();
gfx_set_draw_page(0); gfx_set_draw_page(0);
kid_draw(); kid_draw();
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
@@ -472,16 +499,27 @@ int main(void)
* I — бессмертие Кида (наш отладочный, в оригинале его нет: * I — бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза); * иначе боёвку проверять получается ровно три раза);
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч * 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 k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO); uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD); 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 && k && !cheat_prev) pop_guard_kill();
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal; if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
if (pop_cheats && s && !sword_prev) pop_have_sword = 1; 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; cheat_prev = k;
immo_prev = i; immo_prev = i;
sword_prev = s; sword_prev = s;
nlvl_prev = nl;
} }
if (frozen) { if (frozen) {
PROF(0); /* чёрный: ждём vsync */ PROF(0); /* чёрный: ждём vsync */
@@ -545,7 +583,7 @@ int main(void)
enter_room(down); /* координаты Kid уже репроецированы pop_map */ enter_room(down); /* координаты Kid уже репроецированы pop_map */
gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */ gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */
} else { /* выпал из уровня (нет комнаты снизу) — рестарт */ } else { /* выпал из уровня (нет комнаты снизу) — рестарт */
pop_start_level(LEVEL_ENTRY_POSE); pop_start_level();
gfx_set_draw_page(back); gfx_set_draw_page(back);
} }
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */ } else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
@@ -581,15 +619,25 @@ int main(void)
} }
} }
} }
if (pop_next_level) { /* END_LEVEL: доиграл seq_70 в двери уровня */ if (pop_next_level != pop_current_level) {
/* Порт play_level_2 (seg003:0386): цикл уровня кончается, когда /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81):
* next_level разошёлся с current_level, и start_game грузит * цикл уровня кончается, когда next_level разошёлся с
* следующий. Уровень у нас пока один, поэтому «следующий» = тот * current_level (END_LEVEL после seq_70 в двери уровня либо чит
* же самый — зато вся цепочка (дверь → seq_70 → END_LEVEL → * Shift+L), и play_level грузит следующий.
* конец уровня) работает целиком и проверяема. Загрузка уровня *
* 2 подключается ровно сюда, см. ../docs/levels_plan.md §2.2. */ * HP переносится: hitp_beg_lev = hitp_max ДО загрузки — так
pop_next_level = 0; * поднятая на уровне «жизнь» (большая склянка) переезжает
pop_start_level(LEVEL_ENTRY_POSE); * дальше, а урон — нет (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); gfx_set_draw_page(back);
} }
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
@@ -597,7 +645,7 @@ int main(void)
if (dead_frames > RESPAWN_SETTLE && if (dead_frames > RESPAWN_SETTLE &&
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) { (kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
dead_frames = 0; dead_frames = 0;
pop_start_level(LEVEL_ENTRY_POSE); pop_start_level();
gfx_set_draw_page(back); gfx_set_draw_page(back);
} }
} }