Зелье медленного падения (перо) + цвет пузырьков по типу зелья

L7-FEATHER: тип 3 (уровень 7, комната 1) больше не пустой TODO.

- pop_map.c: pop_feather — счётчик кадров эффекта (POP_FEATHER_FRAMES = 225,
  ванильный порог do_timers seg003:0517; привязку к звуку взять неоткуда).
  fall_accel — ускорение 1 / потолок 4 (seg006:057C) вместо 3 / 33.
  proc_get_object case 3: взвести эффект + зелёная вспышка на 3 кадра.
- pop_kid.c: опкод JMP_IF_FEATHER (0xF7) больше не пропускает адрес
  безусловно — под пером прыгает по нему, то есть seqtbl уводит падение и
  удар в ветки stepfloat/bumpfloat (плавные кадры, без урона).
- roomtest.c: pop_flash_red -> pop_flash_color (жёлтый/красный/ЗЕЛЁНЫЙ);
  сброс pop_feather в pop_start_level (seg003:189).
- Цвет пузырька по ТИПУ зелья (seg008:652), чего у нас не было вовсе:
  3/4 зелёный, 5/6 СИНИЙ, остальные красный.  Mono-блиттера с параметром
  цвета в libbgi нет, поэтому цвет запекается при упаковке: pop_pack_bg.py
  кладёт те же 7 кадров ещё дважды (id 30..36 зелёные, 40..46 синие),
  pop_potion_draw выбирает набор.  Атлас 23 -> 37 спрайтов (+423 Б).
- Синее зелье «−HP» приведено к оригиналу (seg006:1892): своей вспышки не
  ставит (красный кадр даёт общий flash_if_hurt — иначе экран красился
  дважды), а на уровне зелий забирает ПОЛОВИНУ запаса HP.

Такое зелье стоит уже на пройденном уровне 2 (комната 13, тайл (1,3)),
а также ур.8 комн.2 и весь ур.15 — до сих пор оно было красным.

Тесты: phys_feather_fall_is_slow_and_harmless (обычное падение с двух рядов
разгоняется и стоит HP, под пером скорость <= 4 и HP целое); tests-host
5/5 (1727 в [phys]), size-check OK.

Расхождения с ванилью — в docs/impl_diff.md (перо ловит только Кида;
синее зелье без своей вспышки).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 12:13:29 +03:00
parent b5951c9b5c
commit d7d18aef77
13 changed files with 267 additions and 24 deletions
+38
View File
@@ -171,3 +171,41 @@ for (short column = 9; column >= 0; --column) {
направления: нажать её из комнаты 11 и войти в 24; и наоборот — войти в 24 направления: нажать её из комнаты 11 и войти в 24; и наоборот — войти в 24
поверху и наступить на неё, стоя в шве. Картинка кнопки обязана совпадать поверху и наступить на неё, стоя в шве. Картинка кнопки обязана совпадать
со статусом ворот в обоих случаях. со статусом ворот в обоих случаях.
---
## Перо (медленное падение) ловит ТОЛЬКО Кида
**Оригинал** (`fall_accel`, seg006:057C): `is_feather_fall` — глобальный флаг,
и медленное падение достаётся ЛЮБОМУ персонажу, который окажется в `Char`,
пока эффект жив. То есть страж, сошедший с уступа в те же секунды, парит
вместе с Кидом, хотя зелье пил не он. SDLPoP считает это багом и чинит
опцией `fix_feather_fall_affects_guards`.
**Мы** берём поведение С ФИКСОМ: `pop_feather` проверяется вместе с
`Char.charid == CHARID_0_KID` — и в `fall_accel` (`pop_map.c`), и в опкоде
`JMP_IF_FEATHER` интерпретатора seqtbl (`pop_kid.c`), чтобы физика и анимация
не разъехались.
**Чем платим.** Сцена, где страж падает при живом пере, будет выглядеть иначе,
чем в DOS-оригинале (у нас он падает нормально, там — парит). На уровне 7,
единственном с этим зельем, такой сцены нет: зелье в комнате 1, стражи — в
других комнатах.
**Что проверять при регрессе.** Уровень 7: выпить зелье в комнате 1, тут же
столкнуть стража в провал — он обязан падать БЫСТРО, а Кид рядом — медленно.
---
## Синее зелье («−HP») не ставит свою вспышку
**Оригинал** (`proc_get_object`, seg006:1892): ветка `case 5` только глушит
звуки, играет `sound_13_kid_hurt` и ставит `hitp_delta`. Экран краснеет не
здесь, а общим механизмом «Кид ранен» (`flash_if_hurt`, seg003:0AFC).
**Мы** раньше ставили в этой ветке ещё и `pop_flash_*` (красную вспышку на 2
кадра) — то есть красили экран дважды: своей вспышкой и кадром урона.
Приведено к оригиналу: ветка правит только `hitp_delta`, краснеет `pop_kid_hurt`.
**Что проверять при регрессе.** Уровень 2, комната 13, зелье `(1,3)`: выпить —
HP убавляется на единицу, экран краснеет РОВНО один раз (без двойного строба).
@@ -37,6 +37,11 @@ static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_til
#endif /* POP_BG_ATLAS_NAMES */ #endif /* POP_BG_ATLAS_NAMES */
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */ #define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */
#define POP_PAL_POT 0x40 #define POP_PAL_POT 0x40
/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),
зелёный (перо/переворот) и синий (вред/открыть) = те же кадры
под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */
#define POP_POT_BUBB_GREEN 30
#define POP_POT_BUBB_BLUE 40
#define POP_BG_PAL "pop_bg.pal" #define POP_BG_PAL "pop_bg.pal"
#endif #endif
Binary file not shown.
+31
View File
@@ -58,6 +58,37 @@
3. Хост-тест `t_grab.c` уже умеет мерить ширину окна (карта исходов по фазе X и 3. Хост-тест `t_grab.c` уже умеет мерить ширину окна (карта исходов по фазе X и
задержке нажатия) — на нём и проверять «стало стабильнее». задержке нажатия) — на нём и проверять «стало стабильнее».
### Протокол замера (написан 2026-08-12, чтобы следующий заход начался с фактов)
Пользователь связывает с этим же и «двойные срабатывания не вовремя». Прежде
чем трогать драйвер — доказать, что он вообще виноват: **сегодняшний случай
«Кид ходит как с зажатым Shift» оказался артефактом MCP-моста, а не драйвера**
(`pause` освободил 6 незакрытых входов, которые плагин переустанавливал каждый
кадр; как только они отпустились, драйвер снял бит сам). То есть break-коды
драйвер обрабатывает верно, и ложная тревога здесь стоила часа.
Что мерить:
1. **Поток скан-кодов**: кольцевой лог в трамплине прерывания
(`libc/irq/_irq_tramp.c` / `kbd_raw_poll.c`) — байт кода, флаг make/break,
номер кадра. Читать из MAME по адресу буфера (`mem`), как читаем `_Kid`.
2. **Что увидел движок**: `pop_ctrl_shift_held()` и битмап `_kbdraw_down`
(адрес — из `.map`, для roomtest 0xAEF9) на тех же кадрах. Расхождение
«код пришёл, а движок не увидел» = потеря в драйвере; «кода не было, а бит
стоит» = потеря break (то, что подозревает пользователь).
3. **Сцены**: бег с отпусканием, разворот, вис + отпускание Shift, падение с
зажатым заранее Shift (вход на уровень 7), быстрое повторное нажатие
(проверка «двойного срабатывания» — typematic, см. memory
`kbd_overrun_wipe_modifiers`).
Что считать нормой: каждому make ровно один break; между make и видимым
`kbd_raw_down()` — не больше кадра; при удержании typematic-повторы НЕ должны
доходить до `pop_ctrl` как новые нажатия (диспетчер ждёт фронт).
Отдельная гипотеза, которую замер тоже закроет: окно зацепа отмеряется В
КАДРАХ, а игра идёт на ~39 % быстрее оригинала ([L1-SPEED](TASKS_OPEN.md#l1-speed))
— тогда «менее стабильно» это не клавиатура, а темп, и чинить надо темп.
--- ---
<a id="уровень-2"></a> <a id="уровень-2"></a>
+8 -2
View File
@@ -89,8 +89,14 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1) ### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1)
**Задача (пользователь, 2026-08-12): составить план — здесь он и есть; код > **Статус 2026-08-12: код написан, ждёт живой проверки в MAME.** Сделаны все
следующим заходом.** > шесть шагов ниже плюс синие пузырьки зелья «−HP» (тип 5) — они всплыли по
> ходу: пользователь напомнил, что такое зелье стоит уже на уровне 2 (комната
> 13, тайл `(1,3)`; ещё ур.8 комн.2 и весь ур.15), а цвет у него был красный.
> Хост-тесты: `phys_feather_fall_is_slow_and_harmless` (скорость ≤ 4 и HP
> целое против обычного падения с двух рядов). **Осталось**: проверить в MAME
> — ур.7 комн.1 (зелёные пузырьки, зелёная вспышка, медленный спуск в шахту) и
> ур.2 комн.13 (синие пузырьки, −1 HP, экран краснеет один раз).
Зелье на `(2,8)` комнаты 1 уровня 7 (сверено по `res2007.bin`: modifier 3, то Зелье на `(2,8)` комнаты 1 уровня 7 (сверено по `res2007.bin`: modifier 3, то
есть `potion_type = 3` — «slow fall»). Сейчас `pop_proc_get_object` есть `potion_type = 3` — «slow fall»). Сейчас `pop_proc_get_object`
+10 -1
View File
@@ -426,9 +426,18 @@ void play_seq(void)
case 0xFE: /* FLIP */ case 0xFE: /* FLIP */
Char.direction = (int8_t)~Char.direction; Char.direction = (int8_t)~Char.direction;
break; break;
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */ case 0xF7: /* JMP_IF_FEATHER (seg006:586): под зельем медленного
* падения — перейти по адресу (в seqtbl это ветки
* stepfloat/bumpfloat: «парящие» кадры падения и удара,
* без урона), иначе пропустить адрес и идти дальше.
* Эффект достаётся только Киду — как и сама физика пера
* (fall_accel, pop_map.c). */
if (!pop_feather || Char.charid != CHARID_0_KID) {
Char.curr_seq += 2; Char.curr_seq += 2;
break; break;
}
/* иначе — ПРОВАЛ в JMP ниже: адрес лежит ровно там же */
/* fallthrough */
case 0xFF: { /* JMP */ case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) | uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8)); ((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
+50 -10
View File
@@ -118,6 +118,10 @@
#define SCREENSPACE_X 58 #define SCREENSPACE_X 58
#define FALL_ACCEL 3 #define FALL_ACCEL 3
#define FALL_MAX 33 #define FALL_MAX 33
/* Перо (seg006:057C, types.h:1435): ускорение и потолок скорости падения
* под зельем медленного падения. */
#define FALL_ACCEL_FEATHER 1
#define FALL_MAX_FEATHER 4
#define TILE_MIDX 7 #define TILE_MIDX 7
#define FRAME_THIN 0x20 #define FRAME_THIN 0x20
@@ -158,6 +162,11 @@ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */ 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;
/* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам /* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам
* (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом * (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом
* с check_collisions. Сброс при смене комнаты — coll_prev_row. */ * с check_collisions. Сброс при смене комнаты — coll_prev_row. */
@@ -823,8 +832,19 @@ static void do_fall(void)
static void fall_accel(void) static void fall_accel(void)
{ {
if (Char.action == ACT_FREEFALL) { if (Char.action == ACT_FREEFALL) {
int fy = (uint8_t)Char.fall_y + FALL_ACCEL; 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; if (fy > FALL_MAX) fy = FALL_MAX;
}
Char.fall_y = (int8_t)fy; Char.fall_y = (int8_t)fy;
} }
} }
@@ -1157,7 +1177,7 @@ uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 подн
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */ uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это /* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
* flash_color; нам хватает двух значений, которые реально встречаются. */ * flash_color; нам хватает двух значений, которые реально встречаются. */
uint8_t pop_flash_red; uint8_t pop_flash_color;
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */ static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить /* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
@@ -1222,22 +1242,38 @@ void pop_proc_get_object(void) __banked
case 1: /* здоровье: +1 HP, красная вспышка */ case 1: /* здоровье: +1 HP, красная вспышка */
if (hitp_curr != hitp_max) { if (hitp_curr != hitp_max) {
hitp_delta = 1; hitp_delta = 1;
pop_flash_red = 1; pop_flash_color = POP_FLASH_RED;
pop_flash_time = 2; pop_flash_time = 2;
} }
break; break;
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */ case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
pop_kid_add_life(); pop_kid_add_life();
pop_flash_red = 1; pop_flash_color = POP_FLASH_RED;
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */ pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
break; break;
case 5: /* «злое» зелье: −1 HP */ case 3: /* ПЕРО: медленное падение */
hitp_delta = -1; /* feather_fall (seg000:15F8): взвести эффект + ЗЕЛЁНАЯ вспышка
pop_flash_red = 1; * на 3 кадра (flash_color = 2). Оригинал здесь же глушит звуки
pop_flash_time = 2; * и играет sound_39_low_weight — по нему же меряет срок; у нас
* срок задан кадрами (POP_FEATHER_FRAMES). */
pop_feather = POP_FEATHER_FRAMES;
pop_flash_color = POP_FLASH_GREEN;
pop_flash_time = 3;
break; break;
default: /* 3 перо / 4 переворот / 6 открыть — case 5: /* «злое» зелье (синее): −1 HP */
* TODO вместе с их уровнями (7/9) */ /* 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: /* 4 переворот / 6 открыть — TODO
* вместе с их уровнями (9/13) */
break; break;
} }
} }
@@ -2556,6 +2592,10 @@ static void check_chomped_guard(void)
static void kid_phys(void) static void kid_phys(void)
{ {
if (grab_timer) grab_timer--; if (grab_timer) grab_timer--;
/* do_timers (seg003:0517): срок действия пера тикает раз в игровой кадр.
* Место — у Кида, а не у стража: эффект в оригинале снимается общим
* таймером главного цикла, а у нас единственный кадровый тик Кида. */
if (pop_feather) pop_feather--;
fall_accel(); fall_accel();
fall_speed(); fall_speed();
determine_col(); determine_col();
+11 -2
View File
@@ -84,8 +84,17 @@ extern uint8_t pop_item_taken;
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока /* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */ * НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
extern uint8_t pop_flash_time; extern uint8_t pop_flash_time;
/* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */ /* Цвет вспышки (порт flash_color, seg006 proc_get_object / seg000 feather_fall):
extern uint8_t pop_flash_red; * жёлтая подобран меч, красная зелья HP, зелёная перо. */
#define POP_FLASH_YELLOW 0
#define POP_FLASH_RED 1
#define POP_FLASH_GREEN 2
extern uint8_t pop_flash_color;
/* is_feather_fall: кадров, пока действует зелье медленного падения. Читают
* play_seq (опкод JMP_IF_FEATHER ветки stepfloat/bumpfloat в seqtbl) и
* приложение (сброс на старте уровня, seg003:189). */
extern uint8_t pop_feather;
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */ uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */ void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
+15 -1
View File
@@ -646,11 +646,25 @@ void pop_potion_draw(int row, int col, uint8_t modif) __banked
int pot_size = (type >= 2 && type < 5) ? 1 : 0; int pot_size = (type >= 2 && type < 5) ? 1 : 0;
int x = (POP_COL_XH[col] + 3) * 8 + 1; int x = (POP_COL_XH[col] + 3) * 8 + 1;
int yb = 63 * row + 62 - (pot_size << 2) - 14; int yb = 63 * row + 62 - (pot_size << 2) - 14;
uint8_t bubb;
if (type == 0) return; if (type == 0) return;
bubb = POTION_FRAM_BUBB[modif & 7];
/* Цвет пузырька по ТИПУ зелья (draw_tile_anim, seg008:652): 3/4 (перо /
* переворот) зелёный, 5/6 (вред / открыть ворота) синий, остальные
* (1 лечение, 2 склянка жизни) красный. У нас цвет запечён в спрайт
* при упаковке (mono-блиттера с параметром цвета нет), поэтому «цвет» =
* другой набор кадров: id 30..36 / 40..46 вместо 16..22 (pop_pack_bg.py).
* Кадр 0 («пузырька нет») общий его подменять нечем и не нужно. */
if (bubb) {
if (type == 3 || type == 4)
bubb = (uint8_t)(POP_POT_BUBB_GREEN + (bubb - POTION_FRAM_BUBB[1]));
else if (type == 5 || type == 6)
bubb = (uint8_t)(POP_POT_BUBB_BLUE + (bubb - POTION_FRAM_BUBB[1]));
}
pop_heal_off(x - 1, yb - 10, 8, 12); pop_heal_off(x - 1, yb - 10, 8, 12);
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */
pop_pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ pop_pot_b(bubb, x, yb); /* кадр пузырька */
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
+16
View File
@@ -79,4 +79,20 @@
#define POP_DBG_SHADOW_NOWAIT 0 #define POP_DBG_SHADOW_NOWAIT 0
#endif #endif
/* ---- Зелье МЕДЛЕННОГО ПАДЕНИЯ (SDLPoP: FEATHER_FALL_LENGTH) ---------- *
* В оригинале длительность эффекта задана НЕ таймером, а звуком: эффект
* держится, пока играет sound_39_low_weight, и снимается принудительно на
* 225-м тике (do_timers, seg003:0517). Звука у нас нет, поэтому берём тот
* же порог напрямую 225 ИГРОВЫХ КАДРОВ (у оригинала тик do_timers = кадр
* игрового цикла; 225 кадров при его 12 fps и есть FEATHER_FALL_LENGTH =
* 18,75 с из фикса SDLPoP). По ВРЕМЕНИ у нас выйдет короче ровно настолько,
* насколько наша игра быстрее оригинала это общий вопрос темпа
* (TASKS_OPEN.md, L1-SPEED), а не свойство зелья. */
#define POP_FEATHER_FRAMES 225
/* ---- Уровень зелий (SDLPoP: «potions level», seg006:1894) ------------ *
* На нём синие склянки «вреда» забирают ПОЛОВИНУ запаса HP, а не единицу.
* В оригинале это условие `current_level == 15` прямо в proc_get_object. */
#define POP_POTIONS_LEVEL 15
#endif /* POP_TUNE_H */ #endif /* POP_TUNE_H */
+7 -5
View File
@@ -443,6 +443,7 @@ static uint8_t pop_checkpoint;
static void pop_start_level(void) static void pop_start_level(void)
{ {
pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */
pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */
uint8_t entry_pose = pop_level_entry_pose(pop_current_level); uint8_t entry_pose = pop_level_entry_pose(pop_current_level);
uint8_t start_room = pop_level_start_room(); uint8_t start_room = pop_level_start_room();
uint8_t pos = pop_level_start_pos(); uint8_t pos = pop_level_start_pos();
@@ -976,12 +977,13 @@ int main(void)
* Приоритет как в оригинале: активный flash_time > кадр урона * Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */ * (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) { if (pop_flash_time) {
/* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color /* Цвет вспышки (flash_color оригинала): меч — ярко-жёлтый, зелья
* оригинала; нам хватает этих двух). */ * HP красный, перо (медленное падение) зелёный. */
if (pop_flash_red) flash_bg(255, 0, 0); /* color_4_red */ if (pop_flash_color == POP_FLASH_RED) flash_bg(255, 0, 0);
else flash_bg(255, 255, 85); /* color_14_brightyellow */ else if (pop_flash_color == POP_FLASH_GREEN) flash_bg(85, 255, 85);
else flash_bg(255, 255, 85);
pop_flash_time--; pop_flash_time--;
if (!pop_flash_time) pop_flash_red = 0; if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW;
flash_on = 1; flash_on = 1;
} else if (pop_kid_hurt) { } else if (pop_kid_hurt) {
flash_bg(224, 0, 48); /* color_12: игра правит его на */ flash_bg(224, 0, 48); /* color_12: игра правит его на */
@@ -21,6 +21,7 @@
#include "stubs.h" #include "stubs.h"
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_map.h" #include "pop_map.h"
#include "pop_guard.h" /* pop_do_delta_hp — урон сводится дельтой */
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */ /* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
#define E 0 #define E 0
@@ -231,6 +232,56 @@ TC_TEST(phys_running_jump_over_3tile_gap)
SC_CHECK(golden_rjump_gap3, "rjump_gap3"); SC_CHECK(golden_rjump_gap3, "rjump_gap3");
} }
/* Падение с ДВУХ рядов: пол только внизу, Кид стартует на кромке ряда 0.
* В обычном падении это 1 HP (land, seg005:173), под пером ни урона, ни
* разгона. */
static const uint8_t room_drop2[30] = {
W, F, E, E, E, E, E, E, E, E,
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, F, F, F, W,
};
/* Максимальная скорость падения за прогон и итоговое HP — через сцену не
* видно (в трассу пишется поза), поэтому меряем прямо по Kid. */
static uint8_t drop2_max_fall_y(uint8_t frames)
{
uint8_t i, mx = 0;
for (i = 0; i < frames; i++) {
sc_trace_clear();
sc_run(SC_R, 1);
if ((uint8_t)Kid.fall_y > mx) mx = (uint8_t)Kid.fall_y;
}
return mx;
}
TC_TEST(phys_feather_fall_is_slow_and_harmless)
{
uint8_t mx_normal, mx_feather, hp_normal, hp_feather;
/* (1) как обычно: разгон до FALL_MAX и −1 HP за два ряда */
sc_room(room_drop2, 1);
sc_kid_at(1, 0, 0);
pop_feather = 0;
mx_normal = drop2_max_fall_y(40);
pop_do_delta_hp(); /* урон копится дельтой, сводит её цикл */
hp_normal = hitp_curr;
/* (2) под зельем медленного падения (seg006:057C: ускорение 1, потолок 4;
* seqtbl уводит приземление в ветку stepfloat без урона) */
sc_room(room_drop2, 1);
sc_kid_at(1, 0, 0);
pop_feather = 200;
mx_feather = drop2_max_fall_y(40);
pop_do_delta_hp();
hp_feather = hitp_curr;
pop_feather = 0;
TC_TRUE(mx_normal > 4); /* обычное падение разгоняется */
TC_TRUE(mx_feather <= 4); /* под пером потолок скорости — 4 */
TC_TRUE(hp_normal < 3); /* два ряда обычно стоят HP */
TC_EQ(hp_feather, 3); /* под пером — целым */
}
/* ---- точка входа ----------------------------------------------------- */ /* ---- точка входа ----------------------------------------------------- */
void main(void) void main(void)
@@ -245,4 +296,5 @@ void main(void)
TC_RUN(phys_run_off_ledge); TC_RUN(phys_run_off_ledge);
TC_RUN(phys_loose_floor_breaks); TC_RUN(phys_loose_floor_breaks);
TC_RUN(phys_running_jump_over_3tile_gap); TC_RUN(phys_running_jump_over_3tile_gap);
TC_RUN(phys_feather_fall_is_slow_and_harmless);
} }
+21
View File
@@ -144,6 +144,15 @@ VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(48,38,20),
(63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal (63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal
POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета) POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета)
POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение») POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение»)
# Цвет пузырька зависит от ТИПА зелья (draw_tile_anim, seg008:652): типы 3/4
# (перо / переворот) — зелёный, 5/6 (вред / открыть) — синий, остальные —
# красный. У нас спрайт красится при УПАКОВКЕ (mono-блиттера с параметром
# цвета в libbgi нет), поэтому каждый цвет — свой набор кадров: те же семь
# силуэтов под сдвинутыми id. Синий появится вместе с зельями 5/6.
POT_BUBBLE_GREEN_COLOR = VGA16_PAL_BASE + 10 # color 10 (seg008:663)
POT_BUBBLE_GREEN_BASE = 30 # id 30..36 = кадры 16..22
POT_BUBBLE_BLUE_COLOR = VGA16_PAL_BASE + 9 # color 9 (seg008:658)
POT_BUBBLE_BLUE_BASE = 40 # id 40..46 = кадры 16..22
CHOMPER_BLOOD_COLOR = VGA16_PAL_BASE + 12 # тот же mono-цвет 12 (seg008:678) CHOMPER_BLOOD_COLOR = VGA16_PAL_BASE + 12 # тот же mono-цвет 12 (seg008:678)
# Кладка ДВОРЦА (wall_pattern, seg008:1928, ветка palace): тело стены — # Кладка ДВОРЦА (wall_pattern, seg008:1928, ветка palace): тело стены —
@@ -475,6 +484,13 @@ def write_header(out_path, env_pages):
"#endif /* POP_BG_ATLAS_NAMES */", "#endif /* POP_BG_ATLAS_NAMES */",
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */', '#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */',
"#define POP_PAL_POT 0x%02X" % POT_PAL_BASE, "#define POP_PAL_POT 0x%02X" % POT_PAL_BASE,
"/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),",
" зелёный (перо/переворот) и синий (вред/открыть) = те же кадры",
" под id %d..%d и %d..%d (draw_tile_anim, seg008:652). */"
% (POT_BUBBLE_GREEN_BASE, POT_BUBBLE_GREEN_BASE + len(POT_BUBBLE_IDS) - 1,
POT_BUBBLE_BLUE_BASE, POT_BUBBLE_BLUE_BASE + len(POT_BUBBLE_IDS) - 1),
"#define POP_POT_BUBB_GREEN %d" % POT_BUBBLE_GREEN_BASE,
"#define POP_POT_BUBB_BLUE %d" % POT_BUBBLE_BLUE_BASE,
'#define POP_BG_PAL "pop_bg.pal"', '#define POP_BG_PAL "pop_bg.pal"',
"", "",
"#endif", "#endif",
@@ -578,6 +594,11 @@ def main():
pot_entries[id] = load_potion(id, opaque=True) pot_entries[id] = load_potion(id, opaque=True)
else: else:
pot_entries[id] = load_potion(id) pot_entries[id] = load_potion(id)
# Наборы пузырьков ДРУГИХ цветов: те же кадры другим mono-цветом.
# зелёный — типы 3/4 (перо, переворот), синий — типы 5/6 (вред, открыть).
for i, id in enumerate(POT_BUBBLE_IDS):
pot_entries[POT_BUBBLE_GREEN_BASE + i] = load_potion(id, POT_BUBBLE_GREEN_COLOR)
pot_entries[POT_BUBBLE_BLUE_BASE + i] = load_potion(id, POT_BUBBLE_BLUE_COLOR)
# chtab_1 (пламя/меч/зелья) от тайлсета НЕ зависит — пакуем один раз. # chtab_1 (пламя/меч/зелья) от тайлсета НЕ зависит — пакуем один раз.
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries) cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries)
total_bytes += sz total_bytes += sz