Уровни 12 и 13: тень, Джафар, падающие плиты + разбор слоёв

Уровень 12 (тень) — порт seg002/seg006:
- check_shadow: подъём тени в комнате 15 по условию «меч подобран»,
  init_shad_12, вход падением (seq 7);
- autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ;
- общий урон (ранил тень — ранил себя) и check_killed_shadow
  (убил тень — убил себя);
- таймер вспышки слияния 42 -> -1, sword_disappears при уходе из
  комнаты 18, появление плит в комнатах 2/13 после слияния.

Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом
попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт
сбросить HP.  Чит навигации по комнатам этот триггер придерживает —
иначе комнату 23 двенадцатого уровня не посмотреть в принципе.

Уровень 13 (Джафар): guard_notice_timer (фора после встречи),
on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три
исключения loose-механики.  Плюс спрайты визиря VIZIER.DAT — без них он
рисовался обычным стражем; заодно цвет из данных комнаты применяется
только к обычному стражу, как в оригинале.

Падающие плиты — семь дефектов, найденных прогонами:
- MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние
  терялись без щебня;
- одиночные сигналы посадки/провала больше не затираются в кадре;
- честный loose_fall: сбитая плита рождается в том же кадре, от места
  удара, с половинной скоростью;
- при снятии плиты метится и сосед справа (висел передний торец);
- heal и отрисовка кусков разнесены на два прохода;
- куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y
  (compare_curr_objs для пары 0x80 сортирует наоборот);
- wall_pattern убран из ceil_over_kid_tile: у оригинала узор из
  draw_tile_bottom идёт в фон, а не в передний слой.

Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44).
TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам.

Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами
тени при слиянии, чит навигации не запускает бесшовный переход, чит K
убивает через штатный путь смерти.

Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER,
GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 18:21:52 +03:00
parent 716f6f9273
commit 67d62e3e8b
37 changed files with 3169 additions and 79 deletions
+216 -14
View File
@@ -28,6 +28,18 @@
#include <fcntl.h>
#include <unistd.h>
/* Фазы loose-плит: массив ТЕКУЩЕЙ комнаты (pop_map) и персистентный per-room
* (pop_trob) плюс постановка trob. Объявлены здесь, а не включением обоих
* заголовков: нужна ровно одна ячейка каждому — падающий кусок срывает
* плиту, в которую врезался (loose_fall, см. mob_break_loose). */
extern uint8_t pop_loose_modif[30];
uint8_t *pop_trob_modif(uint8_t room) __banked;
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked;
/* remove_loose для ОДНОГО тайла — снять плиту с карты немедленно (честный
* loose_fall, см. mob_break_loose). Живёт в pop_map: там рабочая копия
* тайлов комнаты и фазы плит. */
void pop_loose_remove(uint8_t room, uint8_t tilepos) __banked;
/* ---- Тайлсет: подземелье / дворец ----------------------------------- *
* Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один
* и тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE. У нас так
@@ -809,9 +821,17 @@ void pop_loose_bake_empty(int row, int col) __banked
* куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе
* подряд, и второй спавн затирал состояние первого — его пиксели оставались
* висеть в воздухе (на каждой странице на своей y → мерцание), а щебень
* появлялся только под одной плитой. 4 слота: больше одновременно падающих
* плит в одной комнате практически не бывает (в оригинале 14). */
#define MOB_MAX 4
* появлялся только под одной плитой.
*
* Слотов СТОЛЬКО ЖЕ, СКОЛЬКО В ОРИГИНАЛЕ (14). Было 4 с обоснованием
* «больше одновременно падающих плит в одной комнате практически не
* бывает» — неверным ровно с уровня 13: check_fall_flo роняет ШЕСТЬ разом
* (комнаты 23 и 16), а с учётом плит, которые эти куски сбивают по дороге
* (loose_fall), в воздухе бывает и больше. Лишний кусок молча терялся
* (`if (!m) return`), то есть тайл исчезал, а щебня на месте посадки не
* появлялось — найдено прогоном 2026-08-13, комната 16, тайл (1,5).
* Цена — 150 байт W2. */
#define MOB_MAX 14
typedef struct {
uint8_t active; /* 0 нет; 1 падает */
uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */
@@ -823,7 +843,12 @@ typedef struct {
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
} mob_t;
static mob_t mobs[MOB_MAX];
/* НЕ static намеренно: статики банкового модуля в карту символов не
* попадают, и разобрать зависший кадр по слотам (y каждого куска, порядок,
* defer) в отладчике нечем — адреса просто нет. Снято 2026-08-13 для
* разбора артефактов множественного падения; наружу таблица не объявлена и
* никем не читается. */
mob_t mobs[MOB_MAX];
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
* который он лёг → приложение/pop_map ставит debris + do_knock. */
@@ -837,7 +862,7 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF
void pop_loose_mob_spawn(int row, int col) __banked
static void mob_spawn(uint8_t room, int row, int col)
{
uint8_t i;
mob_t *m = 0;
@@ -850,7 +875,7 @@ void pop_loose_mob_spawn(int row, int col) __banked
m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
m->row = (int8_t)row;
m->col = (int8_t)col;
m->room = pop_t_room; /* curmob.room (add_mob, seg007:1010) */
m->room = room; /* curmob.room (add_mob, seg007:1010) */
m->speed = 0;
m->active = 1;
m->clean = 0;
@@ -858,6 +883,55 @@ void pop_loose_mob_spawn(int row, int col) __banked
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
}
/* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */
void pop_loose_mob_spawn(int row, int col) __banked
{
mob_spawn(pop_t_room, row, col);
}
/* Копия куска ОТ ПОЗИЦИИ УДАРА — порт хвоста loose_fall (seg007:1085):
* curmob.y += 6; mob_down_a_row(); add_mob();
* то есть новый кусок заводится не из тайла, а из места, где его сбили:
* на 6 пикселей ниже и рядом ниже, со скоростью УЖЕ ПОДЕЛЁННОЙ пополам.
* Через границу комнаты — как mob_down_a_row: y = 192, ряд 0, комната снизу. */
static void mob_spawn_copy(const mob_t *src)
{
uint8_t i;
mob_t *m = 0;
int16_t y = (int16_t)(src->y + 6);
int8_t row = (int8_t)(src->row + 1);
uint8_t room = src->room;
if (row >= 3) {
y = (int16_t)(y - 192);
row = 0;
room = pop_room_link(room, 3 /* вниз */);
if (!room) return; /* ушло из уровня — куску негде лететь */
}
for (i = 0; i < MOB_MAX; i++)
if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; }
if (!m) return;
m->x = src->x;
m->y = y;
m->row = row;
m->col = src->col;
m->room = room;
m->speed = src->speed; /* уже half (loose_fall делит ДО add_mob) */
m->active = 1;
m->clean = 0;
m->defer = 0;
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
}
/* Отрыв куска в ЧУЖОЙ комнате: плита, которую Кид расшатал и убежал, всё
* равно проваливается (animate_loose в pop_trob крутит её trob по своей
* комнате, как do_trobs оригинала). Летит и садится кусок дальше сам —
* mob_tick_one давно умеет чужую комнату (mob_tile_at / pop_loose_exit). */
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked
{
mob_spawn(room, row, col);
}
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
* с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
@@ -905,6 +979,29 @@ static uint8_t mob_tile_at(mob_t *m)
return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F);
}
/* loose_fall (seg007:1082) — падающий кусок пробил ЕЩЁ ОДНУ расшатанную
* плиту. Порт дословный, три действия В ОДНОМ КАДРЕ:
* 1. налетевший кусок тормозится вдвое (`curmob.speed >>= 1`);
* 2. сбитый тайл снимается с карты немедленно (`remove_loose`);
* 3. на месте удара заводится ВТОРОЙ кусок — копия налетевшего, сдвинутая
* на 6 пикселей и ряд вниз (`curmob.y += 6; mob_down_a_row; add_mob`).
* То есть в столбце дальше летят ДВА куска, и это правильное поведение
* оригинала, а не дефект.
*
* Здесь стоял обходной путь: плите ставилась фаза «падает следующим кадром»,
* а всю работу делал pop_loose_tick. Цена оценивалась как «один кадр
* задержки» и оказалась выше: защита «один сигнал pop_loose_fell на кадр»
* придерживает провал, пока главный цикл не разберёт предыдущий, и на 13-м
* уровне (плиты сыплются пачкой) вторая плита появлялась через несколько
* кадров — уже после того, как первая улетела с экрана, — да ещё и с высоты
* покоя тайла, а не от места удара (найдено прогоном 2026-08-13). */
static void mob_break_loose(mob_t *m)
{
m->speed >>= 1;
pop_loose_remove(m->room, (uint8_t)(m->row * 10 + m->col));
mob_spawn_copy(m);
}
static void mob_tick_one(mob_t *m, uint8_t pg)
{
uint8_t here;
@@ -921,6 +1018,19 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* на одной из двух страниц дабл-буфера — «застрявшая плита», видимая
* через кадр (BUG-MOB-STALE-PAGE). */
if (m->prev_y[pg] != MOB_Y_NONE) {
/* Ширина/высота — по СЛЕДУ, а не по спрайту. Спрайт куска целиком
* влезает в 64x32 (41: 32x13 с y-3, 42: 26x15 с x+32,y-1, 43: 32x3),
* но mob_render рисует ещё и ЦЕЛЫЙ соседний тайл поверх правой части
* (окклюзия грани пола), причём в GFX_BANK_SPRITE — то есть мимо
* теневой копии фона. Всё, что тот тайл нарисовал вне коридора
* heal'а, не стиралось никогда: кусок улетал, а в соседнем столбце
* оставался обрезок пола (найдено прогоном 2026-08-13). Коридор
* расширен до площади, которую реально может тронуть эта
* перерисовка: столбец соседа по ширине и ряд по высоте.
*
* ЗАДЕЛ: правильнее не перерисовывать соседа вовсе, а КЛИПОВАТЬ
* правую часть куска — оригинал так и делает (add_mob_to_objtable,
* seg007:1161: clip.right = 40). Тогда коридор вернётся к 64x32. */
pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
m->prev_y[pg] = MOB_Y_NONE;
}
@@ -929,11 +1039,19 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
if (m->speed < 29) m->speed += 3;
m->y += m->speed;
/* move_loose (seg007:1126): низ куска пересёк границу своего ряда —
* смотрим тайл этого ряда. Пусто/loose — валится дальше (порт
* loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
* пол — приземление (loose_land: debris + do_knock). */
* смотрим тайл этого ряда. Пусто/loose — валится дальше, пол —
* приземление (loose_land: debris + do_knock). Через LOOSE кусок именно
* ПРОЛЕТАЕТ, как через пустоту, — но по дороге СРЫВАЕТ её (loose_fall,
* seg007:1082).
*
* Здесь раньше стояло «порт loose_fall не нужен: в ряду ниже её нет» —
* допущение, верное для уровней 1-11 и сломавшееся на 13-м: комната 16,
* плиты (1,6) и (1,7), на которые сверху сыплется гряда из комнаты 1.
* Куски пролетали сквозь них правильно, а сами плиты оставались целыми и
* нарисованными (найдено прогоном 2026-08-13). */
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
uint8_t t = mob_tile_at(m);
if (t == 11) mob_break_loose(m);
if (t == 0 || t == 11) {
/* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда —
* в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает
@@ -953,6 +1071,14 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
}
here = (uint8_t)(m->room == pop_t_room);
}
} else if (!here && pop_loose_exit) {
/* Сигнал прошлой посадки в чужой комнате ещё не разобран (он
* ОДИН на кадр) — держим кусок на границе ряда и садим следующим
* кадром, как это уже сделано для pop_loose_landed ниже. Без
* этого второй кусок затирал первый, и из нескольких плит,
* улетевших в одну дыру, щебень оставляла только одна (найдено
* прогоном 2026-08-13, уровень 13). */
m->y = MOB_Y_BOUND[m->row + 1];
} else if (!here) {
/* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, но щебень и
* КНОПКА под куском обязаны сработать — сигналим главному циклу
@@ -996,9 +1122,47 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */
else if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col);
else over = (m->y > pop_cd[POP_CH_KID].fpy);
if (over) { m->defer = 1; return; }
/* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок
* пойдёт. Пока heal и вывод шли в одной итерации, heal СЛЕДУЮЩЕГО
* куска срезал верхнюю кромку уже нарисованного предыдущего: на 13-м
* уровне куски идут лесенкой в соседних столбцах с разницей ~20 px
* по y, и перекрытие гарантировано (артефакт «чёрный бар», найден
* прогоном 2026-08-13, комната 23). Теперь сперва тикают ВСЕ куски
* (то есть heal'ятся все), и только потом рисуются. */
m->defer = over;
}
mob_render(m, pg);
}
/* Один проход отрисовки кусков. want_defer: 0 — те, что под Kid (идут сразу
* после фона), 1 — те, что поверх него (после персонажей).
*
* Порядок внутри прохода — ПО y, но ПО УБЫВАНИЮ: сперва нижние, последним
* верхний. Это НЕ описка и не общее правило «ниже = поверх»: сравнение двух
* объектов в оригинале (compare_curr_objs, seg008:1572) разводит два случая,
*
* оба объекта — падающие плиты (тип 0x80): y1 < y2 → по УБЫВАНИЮ
* любая другая пара: y1 > y2 → по возрастанию
*
* то есть между СОБОЙ куски укладываются наоборот, чем всё остальное: верхний
* ложится поверх нижнего. Видно на паре из loose_fall (сбитая плита летит в
* 6 пикселях под налетевшей): при общем правиле нижняя перекрывала верхнюю,
* и это поймал прогон 2026-08-13.
*
* Порядок «кусок против Кида» здесь ни при чём — его решает defer выше, и там
* действует обычное правило. */
static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
{
uint8_t order[MOB_MAX], n = 0, i, j;
for (i = 0; i < MOB_MAX; i++) {
mob_t *m = &mobs[i];
if (!m->active || m->room != pop_t_room) continue;
if ((uint8_t)(m->defer != 0) != want_defer) continue;
for (j = n; j > 0 && mobs[order[j - 1]].y < m->y; j--)
order[j] = order[j - 1];
order[j] = i;
n++;
}
for (i = 0; i < n; i++) mob_render(&mobs[order[i]], pg);
}
void pop_loose_mob_tick(void) __banked
@@ -1010,6 +1174,15 @@ void pop_loose_mob_tick(void) __banked
* (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а
* стереть себя ему всё равно надо будет. */
if (mobs[i].active || mobs[i].clean)
/* Метка «фон трогали вот здесь» обязана покрывать ВЕСЬ коридор
* heal'а, а не габарит спрайта. Иначе персонаж, попавший в
* расширенную часть коридора, стирается heal'ом, но перерисовать
* себя не просит (pop_char_skip_mask его пропускает) — и просто
* исчезает с экрана. Ровно это и случилось, когда коридор
* расширили под транзитную перерисовку соседа (прогон
* 2026-08-13: в комнате 23 после падения гряды пропадал Кид).
* Запас сверху — на путь, пройденный за кадр (speed до 29): heal
* стирает ПРОШЛУЮ позицию, она выше текущей. */
pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64);
mob_tick_one(&mobs[i], pg);
}
@@ -1042,6 +1215,24 @@ static void mob_render(mob_t *m, uint8_t pg)
* границе рядов. Порт draw_mob (seg007:13E5). */
int8_t r = pop_y_to_row((int16_t)m->y);
int8_t rt = pop_y_to_row((int16_t)(m->y - 18));
/* ОКНО = КОРИДОР HEAL ЭТОГО КУСКА, ни пикселем шире. draw_tile
* рисует тайл ЦЕЛИКОМ (пол, грани, верх кладки ряда ниже — порядка
* 64x63), причём в GFX_BANK_SPRITE, то есть мимо теневой копии фона.
* Всё, что он клал за пределами коридора, не стиралось НИКОГДА:
* кусок улетал, а в соседнем столбце оставался обрезок пола
* (найдено прогоном 2026-08-13).
*
* Расширять под это heal НЕЛЬЗЯ: heal — самая дорогая операция
* кадра, а на 13-м уровне таких кусков одновременно шесть. Поэтому
* режем не heal под перерисовку, а перерисовку под heal — тем же
* окном, которым fore-слой отсекает лишние тайлы. */
pop_t_fclip_x0 = MOB_X0(m->x);
pop_t_fclip_x1 = MOB_X0(m->x) + MOB_W;
pop_t_fclip_y0 = m->y - 27 + POP_YOFF;
pop_t_fclip_y1 = pop_t_fclip_y0 + 32;
pop_t_fclip_ly0 = pop_t_fclip_y0; /* переворота у кусков нет */
pop_t_fclip_ly1 = pop_t_fclip_y1;
pop_t_fclip_on = 1;
if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */
draw_tile(r, m->col + 1);
pop_t_clip_top = 0;
@@ -1050,19 +1241,30 @@ static void mob_render(mob_t *m, uint8_t pg)
draw_tile(rt, m->col + 1);
pop_t_clip_top = 0;
}
pop_t_fclip_on = 0;
}
gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */
pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30);
}
/* Отрисовка кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed(): фон
* кадра обязан быть полностью готов до любого спрайта.
*
* Раньше куски рисовались внутри pop_loose_mob_tick, то есть ДО разбора
* пометок перерисовки, и перепечатка тайла ложилась поверх уже нарисованного
* куска — окно задней стены, передняя грань пола (найдено прогоном
* 2026-08-13, комнаты 16 и 23). */
void pop_loose_mob_draw(void) __banked
{
mob_draw_pass(gfx_get_draw_page() & 1, 0);
}
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
* после отрисовки персонажей, до fore-слоя. */
void pop_loose_mob_draw_over(void) __banked
{
uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++)
if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); }
mob_draw_pass(gfx_get_draw_page() & 1, 1);
}
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и