Уровень 4: skip по маске тайлов + порт loose_land, ворота 0xFF, пламя в фон

ОПТИМИЗАЦИЯ.  Метка «фон трогали» была одним union-прямоугольником на
страницу, и три факела комнаты склеивались в полосу x 40..280 на всю
комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки
и каждый кадр платил полным fore-проходом.  Теперь это маска тайлов
(uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница),
проверка — три AND через резидентный pop_cd_hit.  Гранулярность тайла — это
гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe;
подтайловое уточнение там только по высоте, wipe_heights).  Замер на (1,7):
циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3
растровых кадра.

BUG-LOOSE-BUTTON-1.  Упавшая плита не нажимала кнопку.  Три слоя: порт
loose_land не звал trigger_button вовсе; pop_room_col_landing считала
площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки,
зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего
ворота начинали открываться, пока она ещё в воздухе.  Нажатие в оригинале
ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ»
(modifier 0xFF), и кнопка съедается.  Посадка в комнате снизу переехала на
новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже
поля).

BUG-GATE-FF-1.  0xFF был перегружен: сторожевое «тайла нет» в gate_modif и
живое «открыто навсегда» из trigger_gate.  gate_passable заворачивал Кида в
воротах, нарисованных открытыми.  Мёртвая ветка убрана.

BUG-TORCH-CHOMP-1.  Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и
стирал пламя факела (0,6), которое рисуется в ячейке правого соседа.  Фикс —
запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем
канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера
сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале.
Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE.

Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл
tiles_5_stuck в атласе.

Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1)
-> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host —
все 5 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 17:52:04 +03:00
parent 6a824dd7ab
commit 4718bff767
13 changed files with 364 additions and 47 deletions
+28
View File
@@ -69,6 +69,34 @@ MAME (`z80_profiling_method`) на роомтесте, уровень 1 комн
139 398 (27 %), циан (`PROF(6)`) 233 515 (45 % работы = 54 % растрового
кадра, начинается на 74 % первого кадра и кончается на 123 %).
## СДЕЛАНО 2026-08-10: метка «фон трогали» стала маской ТАЙЛОВ
Было: один union-прямоугольник на страницу. Три факела трогают по пятну
16x18 в колонках 1, 6 и 8, а их объединение — полоса `x 40..280` на всю
комнату; Кид, стоящий где угодно между крайними факелами, в неё попадал и
перерисовывался каждый кадр со всем fore-проходом.
Стало: `uint16_t pop_cd_dmask[2][3]` — бит на колонку, слово на ряд, набор на
страницу. Колонка берётся сдвигом (`x >> 5`), ряд — цепочкой сравнений;
проверка в `cd_quiet` — три `AND` через резидентный `pop_cd_hit`.
Гранулярность тайла — это гранулярность ОРИГИНАЛА: пометки там тоже по
тайлам (`redraw_frames_anim[tilepos]`, `set_wipe`), персонажи привязаны к
тайлу через `tile_object_redraw[tilepos]`, а единственное подтайловое
уточнение (`wipe_heights`) — по высоте, не по ширине. Полутайл (16 px) не дал
бы ничего: пламя рисуется с отступом 8 px и шириной 16, то есть занимает
середину тайла и задевает обе половины.
Замер на той же сцене, где снимался профиль ниже (Кид неподвижно на (1,7)):
| | было | стало |
|---|---:|---:|
| циан (спрайты + fore) | 233 515 | **24 781** |
| работа за кадр | 517 609 | **306 553** |
| период | 4 растровых кадра | **3** |
---
## 0. Дворцовая кладка в fore-проходе — 27 % кадра НА НОЛЬ ПИКСЕЛЕЙ
**Цена: 139 863 такта за кадр (32 % растрового кадра), полезных пикселей —
+90
View File
@@ -13,6 +13,96 @@
---
<a id="bug-loose-button-1"></a>
## BUG-LOOSE-BUTTON-1. Упавшая плита не нажимает кнопку — **ЗАКРЫТ 2026-08-10**
**Симптом (пользователь, уровень 4).** Плита `16(1,1)` падает вниз, в комнате
17 ничего не меняется: кнопка цела, щебня нет, ворота не открылись.
**Корень — три слоя.**
1. Порт `loose_land` (seg007:11E8) у нас вообще не звал `trigger_button`: и в
своей комнате (`pop_map.c`), и в комнате снизу (`roomtest.c`) он только
клал щебень.
2. `pop_room_col_landing` (pop_level.c) считала площадкой **только чистый
пол** (`t == 1`). У оригинала площадок семь — `tiles_1_floor`,
`tiles_2_spike`, `tiles_6_closer`, `tiles_10_potion`, `tiles_15_opener`,
`tiles_19_torch`, `tiles_30_torch_with_debris`; на всём прочем кусок просто
исчезает. Кнопка в набор не входила, поэтому плита растворялась.
3. Сигнал `pop_loose_fell` взводится в момент ОТРЫВА плиты, а по нему главный
цикл и щебень клал, и кнопку жал — ворота начинали открываться, пока плита
ещё в воздухе.
**Механика оригинала (важно, интуиция обманывает).** Плита не «постоянно
давит» на кнопку. Нажатие ОДНО, но с `button_type = tiles_14_debris`, а это у
`trigger_gate` отдельная ветка: возврат 2 → `animate_door` доводит створку и
ставит `curr_modifier = 0xFF` → ворота заморожены открытыми навсегда, обычный
opener их больше не трогает (`if (modifier == 0xFF) return -1`). Сама кнопка
съедается: тайл становится щебнем. На closer — быстрое закрытие и тоже
щебень. На факеле остаётся отдельный тайл `tiles_30_torch_with_debris`.
**Фикс.** Набор площадок расширен до оригинального; `pop_trigger_button`
зовётся в обоих местах с правильным `button_type`; посадка в комнате снизу
переехала с `pop_loose_fell` на новый сигнал `pop_loose_exit`, который
`mob_tick_one` взводит, когда кусок ушёл ниже поля комнаты.
**Осознанное расхождение.** Оригинал через `mob_down_a_row` продолжает
симуляцию куска уже в нижней комнате; у нас он там не летит, а садится сразу.
Задержка получается «высота комнаты», а не «высота комнаты плюс путь до пола
внизу».
---
<a id="bug-gate-ff-1"></a>
## BUG-GATE-FF-1. Ворота, открытые навсегда, нарисованы открытыми, но непроходимы — **ЗАКРЫТ 2026-08-10**
**Симптом.** После BUG-LOOSE-BUTTON-1 ворота `23(0,9)` рисуются поднятыми, но
Кид в них упирается.
**Корень — перегруженное значение.** `gate_modif` (pop_map.c) отдаёт `0xFF`
как сторожевое «тайла нет за краем уровня», а `trigger_gate` ставит тот же
`0xFF` как живой модификатор «открыто НАСОВСЕМ». В `gate_passable` стояло
`if (gm == 0xFF) return 0;`. До сегодняшнего дня значение в игре не
возникало — ветку по щебню никто не запускал.
**Фикс.** Ветка убрана: она мёртвая (нас зовут только когда тайл ТОЧНО
ворота), а `(0xFF >> 2) + 6 = 69` и так больше любого `fph`. Остальные
читатели `gate_modif` трактуют 63 как «открыто» и правок не потребовали.
---
<a id="bug-torch-chomp-1"></a>
## BUG-TORCH-CHOMP-1. Чомпер стирает пламя соседнего факела — **ЗАКРЫТ 2026-08-10**
**Симптом (пользователь).** Комната 23 уровня 4: факел есть, огня нет.
**Корень.** Пламя рисуется в ячейке ПРАВОГО соседа (`x = COL_XH[col+1]*8 + 8`
= 232..248, `y` 33..50), то есть в тайле чомпера (0,7). `pop_chomp_redraw`
начинается с `pop_heal_off(224, 2, 32, 64)` — восстанавливает запечённый фон в
`x 224..255, y 30..93`. Пламя в фон не запекалось и рисовалось в
`GFX_BANK_SPRITE`, поэтому heal его съедал; вдобавок `pop_redraw_needed` идёт
ПОСЛЕ `pop_process_trobs`, так что стиралось только что нарисованное.
**Тупик, в который я сначала свернул.** Вынес отрисовку пламени в отдельный
проход после `pop_redraw_needed` — стало хуже: пламя легло ПОВЕРХ челюстей
(правильный z-порядок существует только внутри общего обхода тайлов, где
`draw_tile_anim_right` идёт до `draw_tile_anim`), плюс проход индексировал
`trob_code[i]` параллельно `trobs[i]`, а `pop_process_trobs` в конце уплотняет
массив — индексы разъезжались, и чужой trob рисовался как факел (пламя из
головы Кида). Проход откачен.
**Фикс (подсказан пользователем).** Пламя ЗАПЕКАЕТСЯ: `pop_torch_draw` пишет
в `GFX_BANK_NORMAL` (видео-ОЗУ + ОЗУ-копия). Это безопасно именно у факела —
все девять кадров лежат на общем канвасе 16×18 и упакованы `opaque=True`, то
есть кадр полностью накрывает предыдущий, протухнуть в копии нечему; и факел
всегда задний план. Тогда heal чомпера возвращает огонь сам, а челюсти
ложатся поверх — тот же z-порядок, что в оригинале, и без лишнего прохода.
Пузырёк ЗЕЛЬЯ так нельзя: он ползёт вверх, себя не накрывает и требует heal —
`pop_potion_draw` остаётся в `GFX_BANK_SPRITE`. Столкнуться с чомпером в
одном тайле зелье не может.
<a id="bug-guard-ix-1"></a>
## BUG-GUARD-IX-1. «Зависание» при бое со стражем — затёртый IX главного цикла — **ЗАКРЫТ 2026-08-10**
+31
View File
@@ -25,6 +25,10 @@
| ~~BUG-GATE-PASS-1~~ | проход сквозь закрытую решётку шва | Major | **ЗАКРЫТ 2026-08-09** — [bug_closed.md](bug_closed.md#bug-gate-pass-1), смоук уровня 1 пройден |
| ~~BUG-GUARD-IX-1~~ | «зависание» в бою со стражем: затёрт IX главного цикла | Blocker | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-guard-ix-1) |
| ~~BUG-GATE-SEAM-ROW1~~ | решётка в шве не анимируется, если ворота не в ряду 0 | Major | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-gate-seam-row1) |
| ~~BUG-LOOSE-BUTTON-1~~ | упавшая плита не нажимает кнопку под собой | Major | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-loose-button-1) |
| ~~BUG-GATE-FF-1~~ | ворота «открыты навсегда» непроходимы (0xFF перегружен) | Major | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-gate-ff-1) |
| ~~BUG-TORCH-CHOMP-1~~ | чомпер стирает пламя соседнего факела | Minor | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-torch-chomp-1) |
| [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт |
---
@@ -310,3 +314,30 @@ Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал
- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство
данных уровня (разбор — «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md));
приоритет багов в них низкий. Аналогично 23/24 на уровне 3.
---
<a id="died-on-button"></a>
## DIED-ON-BUTTON. `died_on_button` (seg007:776) не портирован
Обнаружено при разборе [BUG-LOOSE-BUTTON-1](bug_closed.md#bug-loose-button-1).
`check_press` (seg006:1707) разбирает ЛЮБОГО мёртвого `Char`, не только Кида:
```c
if (curr_tile2 == tiles_15_opener || curr_tile2 == tiles_6_closer) {
if (Char.alive < 0) trigger_button(1, 0, -1); /* жив */
else died_on_button(); /* мёртв */
}
```
`died_on_button` на **opener** делает тайл обычным полом и форсирует
`button_type = tiles_14_debris` — ворота открываются насовсем; на любой другой
кнопке ставит `tiles_5_stuck` (заклинена, `link_timer == 0x1F`, связь мертва).
У Кида эффект живёт до перезапуска уровня (смерть → `is_restart_level`
`play_level` заново зовёт `load_level()`, тайлы перечитываются), а вот когда на
кнопке умирает СТРАЖ — перезапуска нет, и кнопка заклинена до конца уровня.
Что нужно: сам порт `died_on_button`, константа `TILE_STUCK = 5` и тайл
заклиненной кнопки в атласе фона (сейчас его там нет).
+2 -2
View File
@@ -235,8 +235,8 @@ static uint8_t cd_quiet(uint8_t who, uint8_t p)
int x0, y0, x1, y1;
cd_bbox(who, p, &x0, &y0, &x1, &y1);
y0 += POP_YOFF; y1 += POP_YOFF; /* метка фона — в ЭКРАННЫХ */
if (x0 < pop_cd_dx1[p] && pop_cd_dx0[p] < x1 &&
y0 < pop_cd_dy1[p] && pop_cd_dy0[p] < y1) return 0;
/* cd_bbox отдаёт x1/y1 ИСКЛЮЧИТЕЛЬНО, pop_cd_hit ждёт включительно. */
if (pop_cd_hit(p, x0, y0, x1 - 1, y1 - 1)) return 0;
}
cd_sig_make(who, &g);
a = (const uint8_t *)&g; b = (const uint8_t *)&cd_sig[who][p];
+6 -2
View File
@@ -120,8 +120,12 @@ void pop_char_set_render_dx(uint8_t who, int dx) __banked;
*
* Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья,
* а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */
extern uint8_t pop_cd_dirty; /* биты страниц: прямоугольник взведён */
extern int pop_cd_dx0[2], pop_cd_dy0[2], pop_cd_dx1[2], pop_cd_dy1[2];
extern uint8_t pop_cd_dirty; /* биты страниц: метка взведена */
extern uint16_t pop_cd_dmask[2][3]; /* [страница][ряд] = биты колонок 0..9 */
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
* что трогали на странице p. Резидент (pop_tile.c). */
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1);
void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */
void pop_cd_touch(int x, int y, int w, int h);
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
+8 -1
View File
@@ -243,7 +243,14 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col)
for (row = 0; row < 3; row++) { /* кусок пролетает пустые ряды */
uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
if (t == 0) continue; /* empty — дальше вниз */
if (t == 1) res = row; /* floor — приземление → debris */
/* Набор площадок — дословно switch из loose_land (seg007:11E8):
* пол, пика, обе кнопки, зелье и оба факела. На всём остальном
* (стена, ворота, колонна) кусок просто исчезает. Раньше здесь
* стоял только пол, и плита, падавшая на КНОПКУ в комнате снизу,
* пропадала бесследно: ни щебня, ни нажатия (уровень 4, плита
* 16(1,1) -> кнопка 17(0,1), открывающая ворота 23(0,9)). */
if (t == 1 || t == 2 || t == 6 || t == 10 ||
t == 15 || t == 19 || t == 30) res = row;
break; /* первый не-пустой тайл — стоп */
}
gfx_w0_unmap();
+26 -2
View File
@@ -54,6 +54,8 @@
#define TILE_WALL 20
#define TILE_LOOSE 11 /* проваливающийся пол */
#define TILE_DEBRIS 14 /* щебень (упавший loose) */
#define TILE_TORCH 19 /* факел */
#define TILE_TORCH_DEBRIS 30 /* факел + щебень (loose_land) */
#define TILE_SPIKE 2 /* пики */
#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */
#define TILE_POTION 10 /* зелье */
@@ -319,7 +321,14 @@ uint8_t pop_gate_modif(int8_t col, int8_t row) __banked
static uint8_t gate_passable(int8_t col, int8_t row)
{
uint8_t gm = gate_modif(col, row);
if (gm == 0xFF) return 0;
/* Ветки `if (gm == 0xFF) return 0;` здесь БЫТЬ НЕ ДОЛЖНО: 0xFF — это не
* только сторожевое «тайла нет» из gate_modif, но и живой модификатор
* «ворота открыты НАСОВСЕМ» (trigger_gate по button_type = щебень,
* seg007:623). Значение это появилось только когда заработал порт
* loose_land, и ворота выходили нарисованными открытыми (отрисовка
* клампит modl>188 -> 188), но непроходимыми. Сторож сюда не приходит:
* зовут нас только там, где тайл ТОЧНО ворота, а (0xFF>>2)+6 = 69 и так
* больше любого fph. */
return (uint8_t)((gm >> 2) + 6) >= (uint8_t)CD_ACT->fph;
}
@@ -2126,9 +2135,24 @@ void pop_loose_tick(void) __banked
if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */
uint8_t pos = (uint8_t)(pop_loose_landed - 1);
uint8_t r = 0, c = pos;
uint8_t tt = (uint8_t)(g_fg[pos] & 0x1F); /* тайл, НА который села плита */
pop_loose_landed = 0;
while (c >= 10) { c -= 10; r++; }
g_fg[pos] = TILE_DEBRIS;
/* Плита, севшая на КНОПКУ, её нажимает (seg007:11E8, ветки
* tiles_15_opener / tiles_6_closer). Тонкость: на opener оригинал
* подставляет button_type = tiles_14_debris, а это у trigger_gate
* отдельная ветка «открыть НАСОВСЕМ» (возврат 2 -> animate_door
* доводит створку и ставит modifier 0xFF, после чего ворота
* заморожены и обычный opener их больше не трогает). Сама кнопка
* съедается: тайл станет щебнем ниже, нажаться второй раз нечему.
* Модификатор (индекс LINKLOC) тайл не меняет, берём его как есть. */
if (tt == TILE_OPENER || tt == TILE_CLOSER)
pop_trigger_button(g_room, pos,
(uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
pop_trob_modif(g_room)[pos]);
/* На факеле — отдельный тайл «факел с щебнем» (seg007:1067). */
g_fg[pos] = (uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
? TILE_TORCH_DEBRIS : TILE_DEBRIS);
/* Перерисовка ПО ПЛОЩАДИ ПОЛА (не LOOSE_GONE: та стирает и верх стены
* ряда ниже оставался чёрный прямоугольник под тайлом). Обе
* страницы дабл-буфера. */
+28 -5
View File
@@ -355,9 +355,11 @@ static void draw_tile(int row, int col)
} else if (lcode == 16) { /* дверь уровня */
draw_leveldoor(lmod, xh, dmy);
}
/* Пламя факела (seg008:560, chtab_1 id modifier+1) в фон НЕ запекаем:
* оно анимируется каждый кадр, а heal вернул бы запечённый кадр из-под
* нового. Рисует pop_torch_draw (динамика, pop_process_trobs). */
/* Пламя факела (seg008:560, chtab_1 id modifier+1) здесь, в статичной
* отрисовке тайла, не рисуем его ведёт pop_torch_draw из
* pop_process_trobs каждый кадр. Но ЗАПЕКАЕТСЯ оно всё равно (там
* GFX_BANK_NORMAL): факел чистый задний план, а самонакрытие кадров
* делает запекание безопасным. Подробности в шапке pop_torch_draw. */
}
/* --- draw_tile_bottom (seg008.c:570) --- */
@@ -608,8 +610,22 @@ void pop_torch_draw(int row, int col, uint8_t modif) __banked
/* heal НЕ нужен: оригинал рисует пламя blitters_0_no_transp (seg008
* draw_tile_anim_right), и все 9 кадров лежат на общем канвасе 16x18
* непрозрачный блит сам полностью накрывает предыдущий кадр. Кадры
* упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS). */
gfx_set_bank(GFX_BANK_SPRITE);
* упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS).
*
* И ровно поэтому пламя ЗАПЕКАЕТСЯ (GFX_BANK_NORMAL: видео-ОЗУ + ОЗУ-копия),
* а не рисуется в SPRITE поверх фона. Запечь его безопасно именно из-за
* самонакрытия следующий кадр сотрёт предыдущий сам, «протухнуть» в
* копии нечему. А выигрыш принципиальный: точечные перерисовки соседних
* тайлов начинаются с heal из ОЗУ-копии, и незапечённое пламя они стирали
* чомпер (0,7) комнаты 23 уровня 4 healит x 224..255 / y 30..93 и
* съедал факел (0,6), который рисуется в ячейке правого соседа. С
* запеканием heal возвращает огонь сам, а челюсти ложатся ПОВЕРХ него
* тот же z-порядок, что у оригинала, где draw_tile_anim_right (пламя
* левого соседа) идёт до draw_tile_anim (сам чомпер).
*
* Пузырёк ЗЕЛЬЯ так нельзя: он ползёт вверх и себя не накрывает, ему
* нужен heal pop_potion_draw остаётся в SPRITE. */
gfx_set_bank(GFX_BANK_NORMAL);
pop_pot_b((uint8_t)(modif + 1), x, yb);
gfx_set_bank(GFX_BANK_NORMAL);
}
@@ -889,6 +905,13 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
}
}
if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
/* Приземление в комнате СНИЗУ разбирает главный цикл — но ИМЕННО
* сейчас, когда кусок долетел, а не в момент отрыва плиты: иначе
* кнопка внизу срабатывала, пока плита ещё в воздухе. */
if (!pop_loose_exit) {
pop_loose_exit = (uint8_t)(m->col + 1);
pop_loose_exit_room = m->room;
}
m->active = 0; m->clean = (uint8_t)(here ? 2 : 0);
return;
}
+9
View File
@@ -14,6 +14,15 @@
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
uint8_t pop_loose_landed;
/* Кусок loose УШЁЛ ВНИЗ из комнаты (порт хвоста move_loose, seg007:1126:
* mob_down_a_row переносит его в комнату снизу; у нас симуляция там не
* продолжается, и приземление разбирает главный цикл). 0 = нет, иначе
* col+1; комната, ИЗ которой он ушёл, в pop_loose_exit_room.
* Сигнал отдельный от pop_loose_fell: тот взводится в момент ОТРЫВА плиты
* (там верен только remove_loose), и по нему кнопка внизу нажималась,
* пока кусок ещё летел. */
uint8_t pop_loose_exit;
uint8_t pop_loose_exit_room;
uint8_t pop_droppedout;
/* leveldoor_open (seg007:456): дверь уровня доехала до верха. Ставит
* анимация двери (pop_trob, банк 6), читает спецсобытие подъёма скелета
+6
View File
@@ -14,6 +14,12 @@
* разбирает логика loose-полов (pop_map, W1/W2). */
extern uint8_t pop_loose_landed;
/* Кусок loose ушёл ниже поля своей комнаты: 0 = нет, иначе col+1; комната
* отправления pop_loose_exit_room. Ставит отрисовка mob (банк 7),
* разбирает главный цикл (посадка в комнате снизу). */
extern uint8_t pop_loose_exit;
extern uint8_t pop_loose_exit_room;
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
extern uint8_t pop_immortal;
+91 -27
View File
@@ -158,40 +158,104 @@ uint8_t pop_loose_frame(uint8_t m)
return m;
}
/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- */
uint8_t pop_cd_dirty;
int pop_cd_dx0[2], pop_cd_dy0[2], pop_cd_dx1[2], pop_cd_dy1[2];
/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- *
*
* Метка БИТОВАЯ МАСКА ТАЙЛОВ, по слову на ряд и по набору на страницу.
* Раньше здесь лежал ОДИН union-прямоугольник на страницу, и это убивало
* пропуск перерисовки персонажа: три факела комнаты трогают по пятнышку
* 16x18 в колонках 1, 6 и 8, а их объединение полоса x 40..280 на всю
* комнату. Кид, стоящий где угодно между крайними факелами, попадал в неё
* и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829
* тактов против 20 071, период 4 растровых кадра против 3).
*
* Гранулярность тайла (32 x 63) вместо точного прямоугольника осознанное
* огрубление: факел помечает всю свою колонку по высоте ряда. Персонаж и
* так занимает бОльшую часть высоты ряда, зато касания перестают
* склеиваться. См. docs/impl_diff.md. */
uint8_t pop_cd_dirty;
uint16_t pop_cd_dmask[2][3];
/* Цикл по страницам развёрнут, а x+w / y+h посчитаны один раз. Не
* вкусовщина: индекс-переменная заставляла SDCC считать адрес каждой из
* восьми ячеек сложением, а `x + w` в условии и в присваивании считались
* дважды замер в MAME дал 6 846 тактов на вызов, а зовётся он с КАЖДОГО
* блита фона (два факела в комнате уже 13 700 за кадр). */
/* CD_LOW[n] = n младших единиц: маска пробега колонок c0..c1 считается как
* CD_LOW[c1+1] & ~CD_LOW[c0] два чтения таблицы вместо цикла сдвигов. */
static const uint16_t CD_LOW[11] = {
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF
};
/* Экранный y -> ряд комнаты 0..2 с клампом (полоса кладки у потолка и низ
* стены ложатся на крайние ряды). Цепочкой сравнений, а не делением на 63:
* у SDCC z80 одно деление ~5 400 тактов (memory sdcc_z80_division_hoisting). */
static uint8_t cd_row_of(int y)
{
y -= POP_YOFF;
if (y < 63) return 0;
if (y < 126) return 1;
return 2;
}
/* Колонки прямоугольника [x..x1] (включительно) -> битовая маска. Тайл ровно
* 32 px и начинается с x=0, поэтому колонка просто сдвиг. */
static uint16_t cd_cols_of(int x, int x1)
{
uint8_t c0, c1;
if (x < 0) x = 0;
if (x1 > 319) x1 = 319;
if (x1 < x) return 0; /* весь прямоугольник вне экрана */
c0 = (uint8_t)(x >> 5);
c1 = (uint8_t)(x1 >> 5);
return (uint16_t)(CD_LOW[c1 + 1] & ~CD_LOW[c0]);
}
/* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр),
* поэтому цикл развёрнут: индекс-переменная заставляла SDCC считать адрес
* каждой ячейки сложением. Зовётся с КАЖДОГО блита фона. */
void pop_cd_touch(int x, int y, int w, int h)
{
int x1 = x + w, y1 = y + h;
uint8_t d = pop_cd_dirty;
if (!(d & 1)) {
pop_cd_dx0[0] = x; pop_cd_dy0[0] = y;
pop_cd_dx1[0] = x1; pop_cd_dy1[0] = y1;
} else {
if (x < pop_cd_dx0[0]) pop_cd_dx0[0] = x;
if (y < pop_cd_dy0[0]) pop_cd_dy0[0] = y;
if (x1 > pop_cd_dx1[0]) pop_cd_dx1[0] = x1;
if (y1 > pop_cd_dy1[0]) pop_cd_dy1[0] = y1;
}
if (!(d & 2)) {
pop_cd_dx0[1] = x; pop_cd_dy0[1] = y;
pop_cd_dx1[1] = x1; pop_cd_dy1[1] = y1;
} else {
if (x < pop_cd_dx0[1]) pop_cd_dx0[1] = x;
if (y < pop_cd_dy0[1]) pop_cd_dy0[1] = y;
if (x1 > pop_cd_dx1[1]) pop_cd_dx1[1] = x1;
if (y1 > pop_cd_dy1[1]) pop_cd_dy1[1] = y1;
uint16_t cols = cd_cols_of(x, x + w - 1);
uint8_t r0, r1;
if (!cols) return;
r0 = cd_row_of(y);
r1 = cd_row_of(y + h - 1);
pop_cd_dmask[0][r0] |= cols;
pop_cd_dmask[1][r0] |= cols;
if (r1 != r0) {
pop_cd_dmask[0][r1] |= cols;
pop_cd_dmask[1][r1] |= cols;
if (r1 - r0 > 1) { /* прямоугольник накрыл все три ряда */
pop_cd_dmask[0][1] |= cols;
pop_cd_dmask[1][1] |= cols;
}
}
pop_cd_dirty = 3;
}
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
* вызовом, без трамплина. */
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1)
{
uint16_t cols;
uint8_t r0, r1;
if (!(pop_cd_dirty & (1 << p))) return 0;
cols = cd_cols_of(x0, x1);
if (!cols) return 0;
r0 = cd_row_of(y0);
r1 = cd_row_of(y1);
if (pop_cd_dmask[p][r0] & cols) return 1;
if (r1 != r0) {
if (pop_cd_dmask[p][r1] & cols) return 1;
if (r1 - r0 > 1 && (pop_cd_dmask[p][1] & cols)) return 1;
}
return 0;
}
/* Страница приведена в порядок — снять с неё метку целиком. */
void pop_cd_clear(uint8_t p)
{
pop_cd_dmask[p][0] = pop_cd_dmask[p][1] = pop_cd_dmask[p][2] = 0;
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
}
/* ---- Блит куска атласа ---------------------------------------------- */
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
* и SDCC заводит под них кадр IX; пока он жил в общем теле, за этот кадр
+8 -2
View File
@@ -555,8 +555,14 @@ void pop_process_trobs(uint8_t cur_room) __banked
if (room == cur_room &&
(code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) {
/* Пламя/пузырёк анимируются КАЖДЫЙ кадр и в фон не запечены —
* рисуем поверх (сами делают heal своей области). */
/* Пламя ЗАПЕКАЕТСЯ в фон (GFX_BANK_NORMAL внутри pop_torch_draw),
* пузырёк зелья нет, ему нужен heal. Порядок «пламя раньше
* перерисовок тайлов» обязателен: heal чомпера-соседа вернёт
* запечённый огонь из ОЗУ-копии, а его собственная графика ляжет
* ПОВЕРХ ровно как в оригинале, где draw_tile_anim_right (пламя
* левого соседа) идёт до draw_tile_anim (сам чомпер). Выносить
* отрисовку ПОСЛЕ pop_redraw_needed нельзя: пламя окажется поверх
* челюстей. */
/* Таблицей, а не tp/10 и tp%10: у Z80 деления нет, SDCC зовёт
* __divuchar + __moduchar на два факела это четыре вызова за
* кадр (замер MAME). Тайл всегда 0..29. */
+31 -6
View File
@@ -252,6 +252,10 @@ static uint8_t room_label_redraw;
* терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
* комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS 0x0E
#define TILE_CLOSER 0x06
#define TILE_OPENER 0x0F
#define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E
#define TILE_EMPTY 0x00
#define TILE_FLOOR 0x01
@@ -815,15 +819,36 @@ int main(void)
gfx_set_draw_page(back);
}
}
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
if (pop_loose_fell) { /* remove_loose: сама плита исчезла */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0;
pop_level_set_tile(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
pop_level_set_tile(cur_room, fpos, TILE_EMPTY);
}
if (pop_loose_exit) { /* кусок долетел до низа комнаты — посадка ниже */
uint8_t fcol = (uint8_t)(pop_loose_exit - 1);
uint8_t from = pop_loose_exit_room;
uint8_t down = pop_room_link(from, LINK_DOWN);
pop_loose_exit = 0;
if (down) {
int8_t lrow = pop_room_col_landing(down, fcol);
if (lrow >= 0) pop_level_set_tile(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
if (lrow >= 0) {
uint8_t lpos = (uint8_t)(lrow * 10 + fcol);
uint8_t tt = pop_level_tile(down, lpos);
/* Кусок сел в комнате СНИЗУ — кнопка там нажимается так
* же, как в своей (loose_land, seg007:11E8): на opener
* подставляется button_type = щебень, то есть «открыть
* НАСОВСЕМ» (modifier 0xFF). Второе место того же
* порта: своя комната в pop_map.c. */
if (tt == TILE_OPENER || tt == TILE_CLOSER)
pop_trigger_button(down, lpos,
(uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
pop_trob_modif(down)[lpos]);
/* На факеле остаётся отдельный тайл «факел с щебнем»
* (seg007:1067), а не обычный щебень. */
pop_level_set_tile(down, lpos,
(uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
? TILE_TORCH_DEBRIS : TILE_DEBRIS));
}
}
}
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
@@ -905,7 +930,7 @@ int main(void)
* для неё снят. ПОСЛЕ отрисовки, а не до: слой фона этого же кадра
* взводит бит и обязан быть учтён. Всё, что рисуется ниже
* (mob поверх Кида, борта), взводит его заново само. */
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << back));
pop_cd_clear(back);
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже