diff --git a/applications/PoP/docs/impl_diff.md b/applications/PoP/docs/impl_diff.md
index 8fd2f1d..1518b8c 100644
--- a/applications/PoP/docs/impl_diff.md
+++ b/applications/PoP/docs/impl_diff.md
@@ -171,3 +171,41 @@ for (short column = 9; column >= 0; --column) {
направления: нажать её из комнаты 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 убавляется на единицу, экран краснеет РОВНО один раз (без двойного строба).
diff --git a/applications/PoP/poc/res/bg/pop_bg_atlas.h b/applications/PoP/poc/res/bg/pop_bg_atlas.h
index e62944c..9e861ea 100644
--- a/applications/PoP/poc/res/bg/pop_bg_atlas.h
+++ b/applications/PoP/poc/res/bg/pop_bg_atlas.h
@@ -37,6 +37,11 @@ static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_til
#endif /* POP_BG_ATLAS_NAMES */
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */
#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"
#endif
diff --git a/applications/PoP/poc/res/bg/pop_pot.atl b/applications/PoP/poc/res/bg/pop_pot.atl
index e6c829e..e2e3695 100644
Binary files a/applications/PoP/poc/res/bg/pop_pot.atl and b/applications/PoP/poc/res/bg/pop_pot.atl differ
diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md
index 6ca2b28..2e3126d 100644
--- a/applications/PoP/roomtest/BUGS_OPEN.md
+++ b/applications/PoP/roomtest/BUGS_OPEN.md
@@ -58,6 +58,37 @@
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))
+— тогда «менее стабильно» это не клавиатура, а темп, и чинить надо темп.
+
---
diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md
index 3875f75..3f8c6de 100644
--- a/applications/PoP/roomtest/TASKS_OPEN.md
+++ b/applications/PoP/roomtest/TASKS_OPEN.md
@@ -89,8 +89,14 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
### 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, то
есть `potion_type = 3` — «slow fall»). Сейчас `pop_proc_get_object`
diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c
index e855318..c479dc7 100644
--- a/applications/PoP/roomtest/pop_kid.c
+++ b/applications/PoP/roomtest/pop_kid.c
@@ -426,9 +426,18 @@ void play_seq(void)
case 0xFE: /* FLIP */
Char.direction = (int8_t)~Char.direction;
break;
- case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
- Char.curr_seq += 2;
- break;
+ 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;
+ break;
+ }
+ /* иначе — ПРОВАЛ в JMP ниже: адрес лежит ровно там же */
+ /* fallthrough */
case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c
index 6234863..bed36f1 100644
--- a/applications/PoP/roomtest/pop_map.c
+++ b/applications/PoP/roomtest/pop_map.c
@@ -118,6 +118,10 @@
#define SCREENSPACE_X 58
#define FALL_ACCEL 3
#define FALL_MAX 33
+/* Перо (seg006:057C, types.h:1435): ускорение и потолок скорости падения
+ * под зельем медленного падения. */
+#define FALL_ACCEL_FEATHER 1
+#define FALL_MAX_FEATHER 4
#define TILE_MIDX 7
#define FRAME_THIN 0x20
@@ -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_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
+/* is_feather_fall (seg000:15F8): кадров, пока действует зелье медленного
+ * падения. У оригинала это флаг + счётчик тиков со сроком по звуку; у нас
+ * просто обратный отсчёт кадров (POP_FEATHER_FRAMES, pop_tune.h). Публичный:
+ * его читают play_seq (опкод JMP_IF_FEATHER, pop_kid.c) и приложение. */
+uint8_t pop_feather;
/* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам
* (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом
* с check_collisions. Сброс при смене комнаты — coll_prev_row. */
@@ -823,8 +832,19 @@ static void do_fall(void)
static void fall_accel(void)
{
if (Char.action == ACT_FREEFALL) {
- int fy = (uint8_t)Char.fall_y + FALL_ACCEL;
- if (fy > FALL_MAX) fy = FALL_MAX;
+ int fy;
+ /* Перо (seg006:057C). Оригинал ловит эффектом ЛЮБОГО персонажа в
+ * Char — зелье пьёт только Кид, но флаг глобальный, и страж, упавший
+ * в ту же минуту, тоже парит; SDLPoP чинит это опцией
+ * fix_feather_fall_affects_guards. Берём поведение С ФИКСОМ (только
+ * Кид): расхождение осознанное, запись в ../docs/impl_diff.md. */
+ if (pop_feather && Char.charid == CHARID_0_KID) {
+ fy = (uint8_t)Char.fall_y + FALL_ACCEL_FEATHER;
+ if (fy > FALL_MAX_FEATHER) fy = FALL_MAX_FEATHER;
+ } else {
+ fy = (uint8_t)Char.fall_y + FALL_ACCEL;
+ if (fy > FALL_MAX) fy = FALL_MAX;
+ }
Char.fall_y = (int8_t)fy;
}
}
@@ -1157,7 +1177,7 @@ uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 подн
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
* flash_color; нам хватает двух значений, которые реально встречаются. */
-uint8_t pop_flash_red;
+uint8_t pop_flash_color;
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
@@ -1222,22 +1242,38 @@ void pop_proc_get_object(void) __banked
case 1: /* здоровье: +1 HP, красная вспышка */
if (hitp_curr != hitp_max) {
hitp_delta = 1;
- pop_flash_red = 1;
+ pop_flash_color = POP_FLASH_RED;
pop_flash_time = 2;
}
break;
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
pop_kid_add_life();
- pop_flash_red = 1;
+ pop_flash_color = POP_FLASH_RED;
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
break;
- case 5: /* «злое» зелье: −1 HP */
- hitp_delta = -1;
- pop_flash_red = 1;
- pop_flash_time = 2;
+ case 3: /* ПЕРО: медленное падение */
+ /* feather_fall (seg000:15F8): взвести эффект + ЗЕЛЁНАЯ вспышка
+ * на 3 кадра (flash_color = 2). Оригинал здесь же глушит звуки
+ * и играет sound_39_low_weight — по нему же меряет срок; у нас
+ * срок задан кадрами (POP_FEATHER_FRAMES). */
+ pop_feather = POP_FEATHER_FRAMES;
+ pop_flash_color = POP_FLASH_GREEN;
+ pop_flash_time = 3;
break;
- default: /* 3 перо / 4 переворот / 6 открыть —
- * TODO вместе с их уровнями (7/9) */
+ case 5: /* «злое» зелье (синее): −1 HP */
+ /* seg006:1892. Своей вспышки у этой ветки НЕТ (в отличие от
+ * лечебных): красный кадр даёт общий механизм «Кид ранен»
+ * (flash_if_hurt, seg003:0AFC) — у нас pop_kid_hurt. Раньше мы
+ * ставили вспышку руками, то есть красили экран дважды.
+ * Спецсобытие уровня зелий: там синие склянки забирают ПОЛОВИНУ
+ * запаса, а не одну единицу. */
+ if (pop_current_level == POP_POTIONS_LEVEL)
+ hitp_delta = (int8_t)(-((hitp_max + 1) >> 1));
+ else
+ hitp_delta = -1;
+ break;
+ default: /* 4 переворот / 6 открыть — TODO
+ * вместе с их уровнями (9/13) */
break;
}
}
@@ -2556,6 +2592,10 @@ static void check_chomped_guard(void)
static void kid_phys(void)
{
if (grab_timer) grab_timer--;
+ /* do_timers (seg003:0517): срок действия пера тикает раз в игровой кадр.
+ * Место — у Кида, а не у стража: эффект в оригинале снимается общим
+ * таймером главного цикла, а у нас единственный кадровый тик Кида. */
+ if (pop_feather) pop_feather--;
fall_accel();
fall_speed();
determine_col();
diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h
index ccd2a4a..8a1bbbf 100644
--- a/applications/PoP/roomtest/pop_map.h
+++ b/applications/PoP/roomtest/pop_map.h
@@ -84,8 +84,17 @@ extern uint8_t pop_item_taken;
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
extern uint8_t pop_flash_time;
-/* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
-extern uint8_t pop_flash_red;
+/* Цвет вспышки (порт flash_color, seg006 proc_get_object / seg000 feather_fall):
+ * жёлтая — подобран меч, красная — зелья 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 пить */
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c
index 34e7a83..79bab84 100644
--- a/applications/PoP/roomtest/pop_room.c
+++ b/applications/PoP/roomtest/pop_room.c
@@ -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 x = (POP_COL_XH[col] + 3) * 8 + 1;
int yb = 63 * row + 62 - (pot_size << 2) - 14;
+ uint8_t bubb;
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);
gfx_set_bank(GFX_BANK_SPRITE);
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);
}
diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h
index 90ec176..57e9a5d 100644
--- a/applications/PoP/roomtest/pop_tune.h
+++ b/applications/PoP/roomtest/pop_tune.h
@@ -79,4 +79,20 @@
#define POP_DBG_SHADOW_NOWAIT 0
#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 */
diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c
index 1fca257..c9672fb 100644
--- a/applications/PoP/roomtest/roomtest.c
+++ b/applications/PoP/roomtest/roomtest.c
@@ -443,6 +443,7 @@ static uint8_t pop_checkpoint;
static void pop_start_level(void)
{
pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */
+ pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */
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();
@@ -976,12 +977,13 @@ int main(void)
* Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) {
- /* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color
- * оригинала; нам хватает этих двух). */
- if (pop_flash_red) flash_bg(255, 0, 0); /* color_4_red */
- else flash_bg(255, 255, 85); /* color_14_brightyellow */
+ /* Цвет вспышки (flash_color оригинала): меч — ярко-жёлтый, зелья
+ * HP — красный, перо (медленное падение) — зелёный. */
+ if (pop_flash_color == POP_FLASH_RED) flash_bg(255, 0, 0);
+ else if (pop_flash_color == POP_FLASH_GREEN) flash_bg(85, 255, 85);
+ else flash_bg(255, 255, 85);
pop_flash_time--;
- if (!pop_flash_time) pop_flash_red = 0;
+ if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW;
flash_on = 1;
} else if (pop_kid_hurt) {
flash_bg(224, 0, 48); /* color_12: игра правит его на */
diff --git a/applications/PoP/roomtest/tests-host/t_phys.c b/applications/PoP/roomtest/tests-host/t_phys.c
index dc0f391..cd45f40 100644
--- a/applications/PoP/roomtest/tests-host/t_phys.c
+++ b/applications/PoP/roomtest/tests-host/t_phys.c
@@ -21,6 +21,7 @@
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
+#include "pop_guard.h" /* pop_do_delta_hp — урон сводится дельтой */
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
#define E 0
@@ -231,6 +232,56 @@ TC_TEST(phys_running_jump_over_3tile_gap)
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)
@@ -245,4 +296,5 @@ void main(void)
TC_RUN(phys_run_off_ledge);
TC_RUN(phys_loose_floor_breaks);
TC_RUN(phys_running_jump_over_3tile_gap);
+ TC_RUN(phys_feather_fall_is_slow_and_harmless);
}
diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py
index 1ae531b..a5319bb 100644
--- a/applications/PoP/toolchain/pop_pack_bg.py
+++ b/applications/PoP/toolchain/pop_pack_bg.py
@@ -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
POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета)
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)
# Кладка ДВОРЦА (wall_pattern, seg008:1928, ветка palace): тело стены —
@@ -475,6 +484,13 @@ def write_header(out_path, env_pages):
"#endif /* POP_BG_ATLAS_NAMES */",
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */',
"#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"',
"",
"#endif",
@@ -578,6 +594,11 @@ def main():
pot_entries[id] = load_potion(id, opaque=True)
else:
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 (пламя/меч/зелья) от тайлсета НЕ зависит — пакуем один раз.
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries)
total_bytes += sz