PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат

Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
  poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
  curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
  УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
  стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
  только в кадрах 150..189.  Пока ветка была одна (charid_2_guard), скелет
  получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
  комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.

Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
  guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
  Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
  меняется вместе со стражем.  Грабля: gfx_pal_load отдаёт указатель в BIOS,
  а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.

Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
  Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
  после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
  обратно.  +48 байт W2.

Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
  seg008:1B06) и расширение перебора объединённым прямоугольником
  «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
  кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
  (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
  «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
  вернувшейся кучей костей.

Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.

Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).

Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).

В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 22:11:03 +03:00
parent 18d177407b
commit 6673279cef
55 changed files with 3340 additions and 1304 deletions
+287 -1
View File
@@ -2,7 +2,9 @@
Сюда переезжает всё, что **закрыто**: подтверждённые фиксы, снятые
диагнозы, осознанные решения «не делать». Открытые баги — в
[`bug_list.md`](bug_list.md), текущие задачи — в [`TASKS.md`](TASKS.md).
[`bug_list.md`](bug_list.md), текущие задачи — в
[`TASKS_OPEN.md`](TASKS_OPEN.md), закрытые задачи с протоколами — в
[`TASKS_CLOSED.md`](TASKS_CLOSED.md).
Файл существует не ради истории как таковой: половина записей ниже — это
разбор КОРНЯ (odd-pixel арифметика `char_x`, подстановка тайла нажатой
@@ -11,6 +13,127 @@
---
<a id="bug-guard-color-1"></a>
## BUG-GUARD-COLOR-1. Страж и его полоса HP — всегда одного цвета — **ЗАКРЫТ 2026-08-07**
**Симптом (пользователь, прогон уровня 2).** Комната 4: страж не того цвета,
что в SDLPoP, и полоса его HP тоже.
**Корень.** Оригинал держит ОДИН набор спрайтов стража и подменяет 16 цветов
палитры: `redraw_screen` (seg003:255) зовёт `set_chtab_palette(chtab_5_guard,
&guard_palettes[0x30*curr_guard_color 0x30], 16)` ПЕРЕД отрисовкой комнаты,
где `curr_guard_color = level.guards_color[room1] & 0x0F` (enter_guard,
seg002:184), а `guard_palettes` — ресурс 10 из `PRINCE.DAT` (7 палитр × 16
цветов, 6-битные каналы). У нас `pop_pack_guard.py` брал `COLOR = 2`
константой (цвет стражей уровня 1) и запекал одну палитру в слоты
`0x90..0x9F`. Полоса HP рисуется тем же атласом — отдельного бага не было.
**Фикс.**
1. `pop_pack_guard.py` выгружает ВСЕ 7 палитр в `pop_guard_pal.h`
(7 × 16 × 4 = 448 Б, записи `B,G,R,0` — формат `gfx_pal_load`);
каналы масштабируются `scale6to8`, как в `pop_pack_kid` для `kid.pal`.
2. `pop_guard_set_palette(color)` (`pop_gdraw.c`) заливает 16 слотов в ОБЕ
палитры дабл-буфера; `color == 0` — не трогать (так и оригинал).
3. Зовётся из `pop_guard_enter` сразу после чтения данных комнаты, то есть
ДО отрисовки — как `redraw_screen`. `pop_level_guard` цвет отдавал уже
давно, его просто никто не использовал.
**Грабли, стоившие итерации (записать на будущее).** Первая версия передавала
`gfx_pal_load` указатель прямо на таблицу — а таблица лежит в rodata
БАНКОВОГО модуля, то есть по 0xC000+. `gfx_pal_load` отдаёт указатель в BIOS
(`$A4` через `rst #0x08`), а BIOS читает только `#4000-#BFFF` (корневой
CLAUDE.md). BIOS забирал мусор с чужой страницы, палитра уезжала в тёмное и
страж становился НЕВИДИМЫМ. Лечится копией записи в локальный буфер — стек
гарантированно в W2.
**Проверено в MAME (уровень 2, ROOMNAV):**
| комната | цвет из уровня | что на экране |
|---------|----------------|---------------|
| 11 | 1 | страж сине-фиолетовый, полоса HP синяя |
| 7 | 3 | страж оранжевый, полоса HP оранжевая |
| ур. 1 | 2 | охра, как было (регрессии нет) |
Цвета сходятся с таблицей `pop_guard_pal.h`: цвет 1 = (72,145,255),
цвет 3 = (255,80,0), цвет 2 = (170,48,0).
**Смежное, НЕ входит сюда:** палитра КЛАДКИ уровня 3 (в оригинале зелёная) —
другой механизм и другой ресурс, заведена отдельной задачей
[L3-COLOR](TASKS_OPEN.md#l3-color).
---
<a id="bug-sword-ghost-1"></a>
## BUG-SWORD-GHOST-1. Переход комнаты В БОЮ: Кид прячет меч и дерётся пустой рукой — **ЗАКРЫТ 2026-08-07**
**Проверено в игре (пользователь):** «вроде проблема не воспроизводится —
всё корректно». Замер на замороженном кадре: оба персонажа в комнате 2,
ряд 1, колонки 4 и 7, `Kid.sword = 2`, `Guard.sword = 2`, луч видимости
прошёл, счётчик уборок меча за прогон — **0**.
**Симптом.** Кида вытеснили из комнаты 3 в комнату 2 в бою, он спрятал
меч; страж вошёл следом, и дальше Кид «отбивался» без клинка. Зависимость
от расстояния между Кидом и стражем в момент перехода: близко — бой
продолжался нормально, далеко — меч убирался и страж не шёл, а в
промежутке страж приходил, но меч оставался в ножнах.
**Корень — мнимая стена за краем комнаты, а не боёвка.** Ложным было
`can_guard_see_kid`. Луч видимости (`check_can_guard_see_kid`, seg003:688)
идёт по тайлам между колонками Кида и стража, а колонка считается из `x`:
`floor((x 7 58) / 14)`. На всём диапазоне `x` 0..255 это даёт `col`
от **−5 до 13**, то есть колонка регулярно уходит ЗА комнату. Оригинал
такие колонки резолвит через `find_room_of_tile` (seg006:005D), гуляя по
`roomlinks` на любую глубину; наш `get_tile` знал соседей только на две
колонки (`2..11`), а дальше отдавал `TILE_WALL`. Луч упирался в эту
мнимую стену, `can_guard_see_kid` падал в 0 — и `control_with_sword`
(ветка «противника не видно») штатно убирал меч.
Тот же ноль не давал достать меч обратно: `control_standing` зовёт
`draw_sword` только при `can_guard_see_kid >= 2`. Отсюда и «драка без
меча», и зависимость от расстояния — на самом деле не от расстояния, а от
того, вышла ли колонка за кэш.
**Артефакт (регистратор в памяти, снят с живой сцены).** Брейкпоинт тут
бесполезен — баг ловится руками и редко, поэтому в код были временно
вшиты байты «кто убрал меч и почему оборвался луч»; читались после сцены:
```
src = 2 control_with_sword, ветка «противника не видно»
why = 2 луч упёрся в стену
tile = 0x14 = 20 = TILE_WALL
col = 12 ← колонка стража ЗА кэшем (было −2..11)
kid_col = 8 guard_col = 12
```
**Фикс.** `pop_map` кэширует `fg` соседних комнат слева и справа ЦЕЛИКОМ
(по 30 байт, раскладка комнаты) и резолвит `col` от −10 до 19 в реальный
тайл соседа; за этими пределами — по-прежнему стена (у оригинала там
следующий `roomlink`, у нас край кэша). Данные забирает сама
`pop_map_set_edges(left, right, up, down)`, читая уровень — отдельной
копии в приложении нет. Заодно `gate_modif` перестал читать за границу
массива модификаторов: openness ворот на дальних колонках берётся из
`room_modif` СОСЕДА (`pop_gate_modif`, им же пользуется луч).
Цена: **+48 байт** в W2 (кэш 6+6 → 30+30), код W1 даже уменьшился.
Комнаты сверху/снизу оставлены как были (`above_fg` / `below_fg`): по
вертикали `curr_row` за −1..3 не выходит, мнимых стен там не возникает.
Попутно вернули строку оригинала, потерянную при порте: `control_with_sword`
сбрасывает `holding_sword` для живого Кида (seg005:980) — от неё зависит
индикатор HP стража.
**Регресс:** `tests-host` — все 5 наборов зелёные, характеризационные
трассы Кида не сдвинулись (`[phys] ok: 1723`). Новый набор проверок
`char_tiles_resolve_across_rooms` в `t_char` держит резолв колонок
−11..20 в тайлы соседей и стену на краю кэша — чтобы кэш нельзя было
молча сузить обратно.
**Почему в SDLPoP не воспроизводилось** (пользователь пробовал): там этой
границы просто нет — `find_room_of_tile` уходит по `roomlinks` сколько
нужно, и луч никогда не встречает мнимой стены.
---
## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ЗАКРЫТ 2026-08-05**
**Проверено в игре (пользователь):** «зацеп работает». Физика была верна с
@@ -404,6 +527,7 @@ K3; K2b просто запускает run-jump». Полировкой это
---
<a id="bug-fall-sword-1"></a>
## BUG-FALL-SWORD-1. Отход с мечом в провал: не та последовательность падения — **ЗАКРЫТ 2026-08-04**
**Симптом (приёмка уровня 2, комната 4).** Кид с вынутым мечом отступает
@@ -550,6 +674,7 @@ pop_gframe (кадр СТРАЖА, image 17): dx=-1 weight_x=8
---
<a id="bug-land-sword-1"></a>
## BUG-LAND-SWORD-1. Кид навсегда застревает в приседе после падения с мечом — **ЗАКРЫТ 2026-08-04**
**Симптом (приёмка уровня 2).** Страж ударил Кида, Кид потерял HP,
@@ -1521,3 +1646,164 @@ dir_56_none`. То есть стражи — такая же часть дан
`../docs/room_model_plan.md`): держать `Kid.room` отдельно от `drawn_room` и
рисовать со смещением ±140, как `xpos_in_drawn_room` (seg004:0405). Пока
поводов для этого нет — заводить обратно только по живому наблюдению.
---
# Прогон всех комнат уровней 1 и 2 (пользователь, 2026-08-07) — ЗАКРЫЛ ТРИ РЕВИЗИИ РАЗОМ
Результат прогона: **крупных багов нет**. Тем самым закрыты и переехали сюда
из `bug_list.md` три накопившихся хвоста — чек-листы ручной перепроверки
фиксов, таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений прогона
2026-08-03. Всё, что с прогона 2026-08-07 осталось открытым, — три записи
уровня 2 в [`bug_list.md`](bug_list.md) (BUG-GUARD-COLOR-1,
BUG-GUARD-SPLASH-1, BUG-CHEAT-FIGHT-1); ни одна из них не мешает играть.
Сырые формулировки пользователя лежат рядом: [`bugs_level1.md`](bugs_level1.md)
и [`bugs_level2.md`](bugs_level2.md).
<a id="ручная-перепроверка-2026-08-03"></a>
## Ручная перепроверка фиксов (2026-08-03) — ПРОЙДЕНА
Шесть корней прогона 2026-08-03 были закрыты автоматической проверкой в MAME
(мост `mame-z80`: чтение `_Kid`, потиковые трассы, скриншоты) — этого хватает,
чтобы показать, что конкретный сценарий больше не воспроизводится, но НЕ
хватает, чтобы поймать регрессии в соседней механике. Отсюда список сценариев
ровно в тех формулировках, в которых баги были заведены. Прогон 2026-08-07
прошёл все комнаты уровней 1 и 2 и ни одного из этих симптомов не показал.
| # | что проверялось | ожидаемо |
|---|-----------------|----------|
| 1 | комната 22: выпить зелье (0,6), выйти в 16/23 и вернуться | кувшина нет, пузырька над пустым местом нет |
| 2 | то же для комнат 14 (0,5) и 17 (2,3) | так же |
| 3 | комната 15: подобрать меч (2,2), выйти и вернуться | меча нет; кладка на дальней стене НЕ мигает |
| 4 | комната 12: разбить плиты (0,1)/(0,2) и потолок в 16, выйти-вернуться | остаются разбитыми, проём не закрывается |
| 5 | комната 17 из 23: разбить (1,5)/(1,6), выпить зелье (2,3), вернуться | всё остаётся |
| 6 | **после смерти** зайти в те же комнаты | ВСЁ восстановлено (в оригинале смерть = `load_level`) |
| 7 | комната 12: разбег в закрытую решётку (0,9) с полушага | не проходит насквозь; перелистывание экрана штатно (см. BUG-SEAM-DRAW-1) |
| 8 | комната 6: бег справа налево от (0,9), длинный прыжок (0,6)→(0,7) | не влетает внутрь стены |
| 9 | комната 5: с кнопки (0,6) падение на (2,7), присед, вставание | остаётся в комнате 5 (проверено трассой: `fr=111 x=177` → `seq_47` → `fr=15 x=173`) |
| 10 | комната 5: нажать кнопку (0,4) | поднимаются ОБЕ решётки — (0,5) видно на экране, (0,9) проверять из комнаты 1 |
| 11 | страж (комнаты 3, 21) убивает Кида | смерть доигрывается, респавн в стартовой позиции уровня; цикла «убил-воскрес» нет |
| 12 | клавиатура: долгая игра с Shift+стрелка | ↑ и Shift не залипают; осторожный шаг не превращается в бег |
| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки |
| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные |
Отдельно смотрели **регрессии от порта `check_collisions`** — он трогает всю
горизонтальную коллизию: бамп в стену на бегу и в прыжке, осторожный шаг у
стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме решётки, переходы через
швы (старый BUG-SEAM-PINGPONG). Не всплыло.
<a id="обход-всех-24-комнат-уровня-1"></a>
## Обход всех 24 комнат уровня 1 — ЗАКРЫТ прогоном 2026-08-07
Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо
`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой),
Kid ставится на первый пол. Номер комнаты — полосками в верхнем борте: слева
десятки, справа единицы (`||` `||||` = 24). Инструмент остаётся в сборке:
он же нужен для приёмки уровня 3.
Таблица заполнялась по ходу отладки и осталась незакрытой на 19 строк из 24;
её закрыл ручной прогон всех комнат уровней 1 и 2 (2026-08-07, крупных багов
нет). Ниже — то, что таблица успела зафиксировать: это не «отчёт по
комнатам», а разбор корней, полезный при похожем симптоме.
| комната | статус | что не так |
|---------|--------|------------|
| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты |
| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` |
| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) |
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» выше |
| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
| 13, 18, 24 | недостижимы в игре | свойство данных уровня, разбор — «НЕ БАГИ» выше |
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
## Сырые наблюдения (прогон 2026-08-03) → корень
Одиннадцать формулировок с прогона свелись к шести корням; все шесть закрыты
и проверены в MAME — разборы выше по файлу.
| # | наблюдение (кратко) | корень |
|---|---------------------|--------|
| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 |
| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 |
| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 |
| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 |
| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 |
| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 |
| 7 | залипает ↑ | BUG-KBD-3 |
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 (поправка — BUG-KBD-5) |
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 |
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 |
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 |
Вторая волна того же прогона (вечер) дала ещё четыре наблюдения — BUG-KBD-4,
BUG-RESPAWN-2, BUG-DRAWORDER-1, BUG-LOOSE-2, — все закрыты, разборы выше.
Тогда же закрыт как невоспроизводящийся BUG-SEAM-DRAW-1 и заведён
BUG-GATE-PASS-1, который остаётся открытым (ждёт сценария).
---
<a id="bug-guard-splash-1"></a>
## BUG-GUARD-SPLASH-1. Нет «брызг» при попадании по стражу — **ЗАКРЫТ 2026-08-07**
**Как было заведено (пользователь, 2026-08-07).** При попадании по стражу
должен рисоваться сплеш («звёздочка»), чтобы игрок видел, что удар дошёл до
цели, не переводя взгляд на полосу HP. У Кида такое есть, у стража — нет.
**Корень.** `draw_hurt_splash` (seg006:16CE) в оригинале зовётся ИЗ ДВУХ
мест: `draw_kid` при `hitp_delta < 0` (seg008:1643) и `draw_guard` при
`guardhp_delta < 0` (seg008:1654). У нас была портирована только первая
половина — `kid_draw_splash()`; для стража вызова не было вовсе, хотя
`guardhp_delta` считался и уже использовался.
**Фикс** (`pop_guard.c`, `pop_gdraw.c`, `roomtest.c`):
- флаг `pop_guard_hurt` — симметрия `pop_kid_hurt`: ставится в
`pop_do_delta_hp`, когда `guardhp_delta < 0`, снимается главным циклом
после отрисовки (стража могло не оказаться в отрисованной комнате —
тогда брызги просто пропадают, как в оригинале);
- отрисовка — ВНУТРИ `pop_guard_draw`, между спрайтом стража и клинком:
`draw_guard` кладёт брызги в objtable сразу после стража и ДО клинка, а
порядок записей задаёт порядок рисования. Заодно это решает fore-слой:
`pop_fore_over_char` в конце той же функции накрывает и брызги;
- спрайт — `chtab_5_guard` `obj_id = 1`, то есть **image 1 нашего атласа
стража** (`res752.png`, звезда 28×26): у `add_midtable` аргумент
`obj_id + 1`, а `get_image` вычитает единицу обратно — тот же off-by-one,
что расписан в шапке `pop_pack_guard.py`. У Кида это image 218;
- смещения — три ветки оригинала: кадр 178 (chomped) брызг не даёт вовсе;
185 и 106..110 (смерть/падение) — `obj_y + 4`; 177 (пики) — сдвиг НАЗАД
на 5; иначе `obj_y 11` (у Кида 15: `((charid == kid) << 2) + 11`);
- свой прямоугольник heal по страницам (`qx_l/qvalid`) — брызги живут один
кадр, но стирать их обязана та страница, в которую рисовали.
**Побочно исправлено:** `pop_kid_hurt` взводился при ЛЮБОЙ ненулевой дельте
HP, поэтому зелье здоровья (+1) давало Киду и брызги, и красную вспышку.
И `draw_kid` (seg008:1643), и `flash_if_hurt` (seg003:785) смотрят именно
`hitp_delta < 0` — условие приведено к оригиналу.
**Проверено в MAME (2026-08-07)**, мост `mame-z80`, уровень 1, комната 21,
живой страж (HP 3/3):
```
bp 51BA ← адрес снятия pop_guard_hurt в main: срабатывает ТОЛЬКО
в кадре удара, то есть уже после отрисовки брызг
key k ← чит «убить стража» = guardhp_delta < 0
→ state=stop PC=0x51BA, guardhp: curr=0 max=3 delta=0 hurt=1
bp 8BDF (_gfx_set_visible_page) + out ← домотать до флипа страницы
snap ← звезда нарисована поверх стража
```
Следующие два кадра (обе страницы дабл-буфера) — чисто, следов брызг нет.
Приём на будущее: снятие флага сделано **по факту** (`if (pop_guard_hurt)
pop_guard_hurt = 0;`), а не безусловно — это и экономит запись каждый кадр,
и даёт готовую точку останова для отладки в MAME. Условные брейкпоинты
мост не принимает (`bp ADDR cond` вешает плагин), поэтому точка останова,
срабатывающая сама по себе только в нужном кадре, — рабочий обходной путь.
**Цена:** `_CODE` 24 179 → 24 209 (+30 Б резидента), BANK4 (`pop_gdraw`)
2407 → 3369 (+962 Б, банк занят на 20.6 %, свободно 13 015 Б).
**Цвет брызг** зависит от палитры стража (слоты `0x90..0x9F`), поэтому он
изменится вместе с [BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1) —
отдельной работы не требует.