diff --git a/applications/PoP/docs/size_optimization_plan.md b/applications/PoP/docs/size_optimization_plan.md index 60ab8ed..0c4ed98 100644 --- a/applications/PoP/docs/size_optimization_plan.md +++ b/applications/PoP/docs/size_optimization_plan.md @@ -256,3 +256,36 @@ save/restore. Детально — `pop_banking_architecture`, `graphics_constr `libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`. - `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют оптимизацию, но держать в уме при рефакторе pop_map/pop_bg). + +--- + +## 7. Лишние блиты в горячем пути (добавлено 2026-07-27) + +Найдено при разборе окклюзии по эталону SDLPoP: **наш «передний слой» рисовал +спрайты, которых в оригинале там нет** — это и артефакты, и лишняя работа +каждый кадр. Исправлено: `fore_tile` (вызывается для КАЖДОГО тайла футпринта +Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — переднюю кромку пола; в +оригинале `draw_tile_fore` (seg008:690) добавляет только `add_foretable`-часть, +а `bottom` идёт через `draw_tile_bottom` в backtable (ПОД персонажем). +Итог: −2..4 блита за кадр, `_CODE` −388 Б, ушла «тень» у основания колонны. + +**Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли +этот спрайт в оригинале в ЭТОЙ таблице?):** + +1. `pop_room_draw`/`draw_tile` — вызовы на входе в комнату не критичны по + скорости, но по ним стоит сверить состав слоёв (backtable vs foretable). +2. `overlay_mid_tile` — сейчас точный порт midtable-части `draw_tile2`; + проверить, не рисуем ли `base_id` там, где оригинал его не рисует + (loose: base=0, потому что кадр плиты идёт через `draw_loose` в backtable). +3. `pop_loose_mob_tick` — перерисовка соседнего тайла (`draw_tile(mob_row, + mob_col+1)`) КАЖДЫЙ кадр падения: в оригинале это `set_redraw_full` на + один кадр; можно ограничить только тайлом, который реально пересекается + с куском. +4. `pop_ceil_shake_draw` — heal 64×8 + два `draw_tile(-1,·)` на кадр тряски; + проверить, нужен ли второй тайл (правую грань loose в полосе потолка + оригинал не рисует вовсе — `draw_tile_aboveroom` без `draw_tile_anim_right`). +5. `fore_only_tile` для полосы потолка: вызывается для всех колонок габарита, + а оригинал (`redraw_needed_above`) — только для колонок с флагом + `redraw_frames_above`; сузить до колонок, реально задетых спрайтом. +6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить, + не зовём ли её там, где оригинал ограничивается `wall_fram_main`. diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 655347d..ebab738 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -6,7 +6,7 @@ EXAMPLE := roomtest # small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе # CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). MEMORY ?= small -EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c --w3 pop_map.c --w3 pop_bg.c +EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c --w3 pop_map.c pop_bg.c EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c BG_DIR := $(CURDIR)/../poc/res/bg diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 63c3268..e0fef16 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -15,6 +15,7 @@ #include #include "pop_bg.h" #include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ +#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */ /* ---- Атласы (прямая адресация) ------------------------------------- */ static atlas_t env[5]; @@ -81,6 +82,36 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) /* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ static void heal_off(int x, int y, int w, int h) { gfx_heal(x, y + POP_YOFF, w, h); } +/* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а + * draw_other_overlay/floor_overlay/полоса потолка). Оверлей включается + * только в позах виса/полёта/подъёма и рисует НЕ по прямоугольнику Kid, + * поэтому kid_heal его следы не убирает: на второй странице дабл-буфера + * остаётся кадр «с оверлеем», и он мерцает через кадр. Копим bbox и + * восстанавливаем фон на ОБЕИХ страницах (pop_fore_heal). */ +static int ov_x0, ov_y0, ov_x1, ov_y1; +static uint8_t ov_pending; /* биты страниц, где надо восстановить фон */ + +static void ov_mark(int x, int y, int w, int h) +{ + if (!ov_pending) { ov_x0 = x; ov_y0 = y; ov_x1 = x + w; ov_y1 = y + h; } + else { + if (x < ov_x0) ov_x0 = x; + if (y < ov_y0) ov_y0 = y; + if (x + w > ov_x1) ov_x1 = x + w; + if (y + h > ov_y1) ov_y1 = y + h; + } + ov_pending = 3; /* обе страницы */ +} + +void pop_fore_heal(void) +{ + uint8_t pg = gfx_get_draw_page() & 1; + if (!(ov_pending & (1 << pg))) return; + heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0); + ov_pending = (uint8_t)(ov_pending & ~(1 << pg)); +} + + static void env_b(uint8_t id, int x, int yb) { blit_b(&env[id >> 5], (uint8_t)(id & 31), x, yb); } static void wall_b(uint8_t id, int x, int yb) { blit_b(&wall_at, id, x, yb); } static void fore_b(uint8_t id, int x, int yb) { blit_b(&fore_at, id, x, yb); } @@ -335,6 +366,7 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) * СОСЕД — правую грань через draw_tile_anim_right). При modif==0 (покой/ * статич. запекание) get_loose_frame=0 → базовые кадры 41/42/43. */ extern uint8_t pop_loose_modif[30]; +extern uint8_t pop_ceil_modif[10]; /* плита-потолок: loose ряда 2 комнаты сверху */ static const uint8_t LOOSE_FRAM_LEFT[12] = {41,69,41,70,70,41,41,41,70,70,70,0}; static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0}; @@ -432,11 +464,11 @@ static void draw_tile(int row, int col) } /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В - * верхней полосе (row<0) loose ряда 2 верхнего соседа рисуем статически - * (кадр покоя 43): анимацию/разбитость соседа мы не тянем. --- */ + * верхней полосе (row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху): + * её состояние живёт отдельно, в pop_ceil_modif[col] (pop_map). --- */ if (code == 11) - env_b(row >= 0 ? LOOSE_FRAM_BOTTOM[get_loose_frame(pop_loose_modif[row * 10 + col])] - : LOOSE_FRAM_BOTTOM[0], x, dby); + env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col] + : pop_ceil_modif[col])], x, dby); /* --- draw_tile_base (seg008.c:611). Верхняя полоса (row<0) базу НЕ * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */ @@ -536,6 +568,46 @@ void pop_room_redraw_seam_left(void) bar(0, POP_YOFF, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ gfx_set_bank(GFX_BANK_TRANSPARENT); draw_tile(0, 0); /* фон col0 + бары ворот (живой modif) */ + /* Bar стёр и полосу кладки У ПОТОЛКА (ряд −1 = ряд 2 комнаты сверху, + * draw_tile_aboveroom): без её перерисовки потолок над решёткой пропадал + * после первой же анимации ворот. Клип — как в pop_room_draw. */ + g_clip_top = POP_YOFF; + draw_tile(-1, 0); + g_clip_top = 0; + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- * + * В полосе кладки у потолка (наш ряд −1) виден только НИЗ плиты + * (draw_tile_aboveroom: bottom+loose, всё выше отрезано клипом POP_YOFF). + * Дрожащий кадр берётся из pop_ceil_modif[col] (см. draw_loose выше). + * shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту + * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */ +#define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */ + +void pop_ceil_shake_draw(int col) +{ + int x = COL_XH[col] * 8; + heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ + gfx_set_bank(GFX_BANK_SPRITE); + g_clip_top = POP_YOFF; + draw_tile(-1, col); + if (col + 1 < 10) draw_tile(-1, col + 1); + g_clip_top = 0; + gfx_set_bank(GFX_BANK_NORMAL); +} + +void pop_ceil_bake_empty(int col) +{ + int x = COL_XH[col] * 8; + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H); /* стереть плиту в ОЗУ-копии */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + g_clip_top = POP_YOFF; + draw_tile(-1, col); /* тайл уже EMPTY → колодец наверх */ + if (col + 1 < 10) draw_tile(-1, col + 1); + g_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); } @@ -543,12 +615,12 @@ void pop_room_redraw_seam_left(void) * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ -void pop_button_redraw(int row, int col) +void pop_floor_bake(int row, int col) { int x = COL_XH[col] * 8; int yb = POP_YOFF + 63 * row; - /* Wipe СВОЕЙ ячейки + правой грани кнопки (заходит в 0,3) + перерисовка - * пола. НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда 1 + /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. + * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); @@ -559,6 +631,8 @@ void pop_button_redraw(int row, int col) gfx_set_bank(GFX_BANK_NORMAL); } +void pop_button_redraw(int row, int col) { pop_floor_bake(row, col); } + /* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — @@ -597,20 +671,35 @@ void pop_loose_bake_empty(int row, int col) * кадр 0 loose (41/42/43) на текущей y. Гравитация: speed 0→+3/кадр * (cap 29), y+=speed. Двоебуфер: коридор фиксирован → heal чистит любой * прошлый кадр ЭТОЙ страницы. Ниже поля кусок прячет clip_borders. */ -static uint8_t mob_active; /* 0 нет; 1 падает; 2 = очистка второй страницы */ +static uint8_t mob_active; /* 0 нет; 1 падает */ +static uint8_t mob_clean; /* кадров дочистки после конца полёта (2 = обе стр.) */ +static uint8_t mob_defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */ static int mob_x; /* левый край (COL_XH*8) */ -static int mob_y; /* main-y (dmy покоя), растёт вниз */ +static int mob_y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */ +static int mob_prev_y[2];/* последняя нарисованная y на КАЖДОЙ странице (heal) */ static int8_t mob_speed; static int8_t mob_row, mob_col; /* тайл-позиция куска (для окклюзии соседа) */ +/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на + * который он лёг → приложение/pop_map ставит debris + do_knock. */ +uint8_t pop_loose_landed; + +/* y_loose_land / y_something (seg007): [row+1], row = −1..3. Ряд −1 — + * плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный + * tilepos, у нас — свой ряд полосы у потолка. */ +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) { mob_x = COL_XH[col] * 8; - mob_y = 63 * row + 62; /* dmy покоя — откуда отрывается кусок */ + mob_y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */ mob_row = (int8_t)row; mob_col = (int8_t)col; mob_speed = 0; mob_active = 1; + mob_clean = 0; } /* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26) @@ -623,39 +712,114 @@ void pop_loose_mob_spawn(int row, int col) /* Остановить падающий кусок (при смене комнаты — состояние per-tile не * должно утечь в новую комнату). */ -void pop_loose_mob_reset(void) { mob_active = 0; } +/* Тайл ОБЪЕКТА Kid (set_objtile_at_char, seg006:1833) — в момент обработки + * этого тайла спрайт Kid попадает в midtable. Задаётся в pop_fore_over_kid, + * читается other_overlay_tile (порядок «кто поверх кого»). */ +static int8_t obj_col, obj_row; + +static void mob_render(uint8_t pg); + +void pop_loose_mob_reset(void) +{ + mob_active = 0; mob_clean = 0; mob_defer = 0; + mob_prev_y[0] = mob_prev_y[1] = MOB_Y_NONE; +} void pop_loose_mob_tick(void) { - if (!mob_active) return; - /* стереть прошлый кусок этой страницы + вернуть печёный фон коридора - * (фикс rect [160,192] по ВСЕЙ ширине куска; overflow прошлого кадра - * стёрт его же clip'ом той же ширины). */ - heal_off(MOB_X0(mob_x), 160, MOB_W, 32); - if (mob_active == 2) { mob_active = 0; return; } /* очистили вторую стр. — всё */ + uint8_t pg = gfx_get_draw_page() & 1; + if (!mob_active && !mob_clean) return; + /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой + * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; + * плита-потолок падает через весь ряд 0, поэтому чистим по фактической + * прошлой позиции. */ + if (mob_prev_y[pg] != MOB_Y_NONE) { + heal_off(MOB_X0(mob_x), mob_prev_y[pg] - 27, MOB_W, 32); + mob_prev_y[pg] = MOB_Y_NONE; + } + if (!mob_active) { mob_clean--; return; } /* гравитация (move_loose): ускорение до 29 */ if (mob_speed < 29) mob_speed += 3; mob_y += mob_speed; - if (mob_y - 24 >= POP_PLAYFIELD_H) { mob_active = 2; return; } /* весь ниже поля → +1 кадр очистки */ + /* move_loose (seg007:1126): низ куска пересёк границу своего ряда — + * смотрим тайл этого ряда. Пусто/loose — валится дальше (порт + * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); + * пол — приземление (loose_land: debris + do_knock). */ + if (mob_row < 3 && mob_y < 226 && MOB_Y_BOUND[mob_row + 1] <= mob_y) { + uint8_t t = (uint8_t)(tile_code(mob_row, mob_col) & 0x1F); + if (t == 0 || t == 11) { + mob_row++; /* mob_down_a_row */ + } else { + mob_y = MOB_Y_BOUND[mob_row + 1]; + pop_loose_landed = (uint8_t)(mob_row * 10 + mob_col + 1); + mob_active = 0; mob_clean = 2; /* кусок исчез — на его месте debris */ + return; + } + } + if (mob_y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */ + mob_active = 0; mob_clean = 2; + return; + } + /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ + * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри + * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто + * ниже — тот позже, т.е. поверх). Тайл падающего куска — draw_mob + * (seg007:13E5): row = y_to_row_mod4(y), col = xh>>2. Значит кусок + * поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при + * совпадении тайлов — если он ниже по y. */ + { + int8_t mrow = (int8_t)(((mob_y + 60) / 63) % 4 - 1); /* y_to_row_mod4 */ + uint8_t over; + if (mrow != obj_row) over = (mrow < obj_row); /* ряды идут 2,1,0 */ + else if (mob_col != obj_col) over = (mob_col > obj_col); + else over = (mob_y > kid_fp_obj_y()); + if (over) { mob_defer = 1; return; } + } + mob_render(pg); +} + +static void mob_render(uint8_t pg) +{ /* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска — * left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1, - * заходит в тайл СПРАВА 2,7) на obj_y-1. У нас bottom=dby(mob_y+3) → - * left=mob_y, right=mob_y+2 в mob_x+32 (col+1). */ + * заходит в тайл СПРАВА) на obj_y-1. */ gfx_set_bank(GFX_BANK_SPRITE); - env_b(43, mob_x, mob_y + 3); - env_b(41, mob_x, mob_y); - env_b(42, mob_x + 32, mob_y + 2); + env_b(43, mob_x, mob_y); + env_b(41, mob_x, mob_y - 3); + env_b(42, mob_x + 32, mob_y - 1); + mob_prev_y[pg] = mob_y; /* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob: * set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob). * Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола, - * её правый край ПРЯЧЕТСЯ за передней гранью пола 2,7. Перерисовываем + * её правый край ПРЯЧЕТСЯ за передней гранью пола. Перерисовываем * сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу * до heal'а следующего кадра — виден как хвост во время падения). */ - if (mob_col + 1 < 10) + if (mob_col + 1 < 10) { + if (mob_row < 0) g_clip_top = POP_YOFF; /* полоса у потолка — только срез */ draw_tile(mob_row, mob_col + 1); + g_clip_top = 0; + } gfx_set_bank(GFX_BANK_NORMAL); - /* клип СВОЕГО выхода за низ поля (в статус-полосу) — по ВСЕЙ ширине куска */ - pop_clip_sprite(MOB_X0(mob_x), MOB_W, mob_y - 23 + POP_YOFF, 30); + /* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */ + pop_clip_sprite(MOB_X0(mob_x), MOB_W, mob_y - 26 + POP_YOFF, 30); +} + +/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать + * после kid_draw, до fore-слоя. */ +void pop_loose_mob_draw_over(void) +{ + if (!mob_defer) return; + mob_defer = 0; + mob_render(gfx_get_draw_page() & 1); +} + +/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и + * y (низ куска). 0 — куска нет. */ +uint8_t pop_loose_mob_pos(int *col, int *y) +{ + if (!mob_active) return 0; + *col = mob_col; *y = mob_y; + return 1; } /* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл- @@ -731,37 +895,63 @@ static int8_t y_to_row(int y) /* y_to_row_mod4 (seg006, TILE_SIZEY= return (int8_t)((y + 60) / 63 % 4 - 1); } -static void fore_tile(int row, int col) /* передний слой одного тайла */ +static void fore_only_tile(int row, int col) { uint8_t code = tile_code(row, col); const piece *t; - /* Нажатая кнопка → пол/stuck (как get_tile_to_draw), ИНАЧЕ fore рисует - * переднюю грань КНОПКИ (bottom_id 149) поверх Kid, перекрывая уже - * нарисованную грань пола 43 → «остаток нажатой кнопки» на грани. */ + /* Нажатая кнопка → пол/stuck (get_tile_to_draw), иначе поверх Kid лезет + * передняя грань КНОПКИ. */ if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } t = &tile_table[code]; int x = COL_XH[col] * 8; int dmy = 63 * row + 62; - int dby = dmy + 3; /* = 63*row+65 (draw_tile_bottom) */ - if (code == 20) { /* стена: ОБЕ грани перекрывают Kid */ - uint8_t wmod = wall_modifier(row, col); - wall_b(WALL_FRAM_BOTTOM[wmod], x, dby); /* нижняя грань-решётка (draw_tile_bottom): */ - wall_pattern(row, col, 0); /* перекрывает голову/руки в НИЖНЕЙ части ячейки */ - wall_b(WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань (draw_tile_fore) */ + if (code == 20) { + wall_b(WALL_FRAM_MAIN[wall_modifier(row, col)], x, dmy); wall_pattern(row, col, 1); - } else { - if (t->bottom_id) /* передняя кромка пола (draw_tile_bottom) */ - env_b(t->bottom_id, x, dby); - if (code == 2) { /* пики: fore-кадр поверх Kid (напоротый) */ - uint8_t sm = tile_mod(row, col); - uint8_t sf = (sm & 0x80) ? 5 : sm; - if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf]) - env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); - } - if (t->fore_id) /* передние объекты: колонны/ворота/щебень */ - fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); + return; } + if (code == 2) { /* пики: fore-кадр */ + uint8_t sm = tile_mod(row, col); + uint8_t sf = (sm & 0x80) ? 5 : sm; + if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf]) + env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); + } + if (t->fore_id) + fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); +} + +/* Полоса кладки У ПОТОЛКА поверх Kid — порт redraw_needed_above (seg008:0221). + * Там draw_tile_bottom(1) и draw_loose(1): аргумент 1 = «добавить ещё и в + * FOREtable», т.е. нижняя грань потолка и кадр плиты-потолка рисуются ПОВЕРХ + * персонажа (иначе руки прыгающего Kid лезут на кромку потолка). Всё + * остальное из этой функции (floorright, anim_topright, right) идёт в + * backtable — под персонажем. */ +static void ceil_over_kid_tile(int col) +{ + uint8_t code = tile_code(-1, col); + const piece *t = &tile_table[code]; + int x = COL_XH[col] * 8; + int dby = 63 * (-1) + 65; /* = 2 */ + if (code == 20) { /* draw_tile_bottom: стена */ + wall_b(WALL_FRAM_BOTTOM[wall_modifier(-1, col)], x, dby); + wall_pattern(-1, col, 0); + } else if (t->bottom_id) { + env_b(t->bottom_id, x, dby); + } + if (code == 11) /* draw_loose(1): плита-потолок */ + env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_ceil_modif[col])], x, dby); + fore_only_tile(-1, col); /* draw_tile_fore */ +} + +static void fore_tile(int row, int col) /* передний слой одного тайла */ +{ + /* draw_tile_fore (seg008:690) — ТОЛЬКО add_foretable-часть. Раньше здесь + * рисовалась ещё и передняя кромка пола (bottom_id) «чтобы перекрывала + * Kid», но в оригинале bottom идёт через draw_tile_bottom в backtable + * (под персонажем), а поверх него попадает лишь при draw_other_overlay. + * Из-за лишнего bottom у основания колонны проскакивала «тень». */ + fore_only_tile(row, col); } /* Пол-оверлей при подъёме (draw_floor_overlay, seg008:1457): когда Kid @@ -782,15 +972,70 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx) dmy = 63 * row + 62; dby = dmy + 3; t = &tile_table[code]; + ov_mark(x, 63 * row + 2, 64, 64); env_b(FLOOR_LEFT_OVERLAY[fidx], x, dmy + (code == 5 ? 1 : 0)); if (t->bottom_id) env_b(t->bottom_id, x, dby); /* draw_tile_bottom */ } + +/* draw_tile2 (seg008:1F48) — но ТОЛЬКО та его часть, что реально может лечь + * ПОВЕРХ персонажа. Ключ (подтверждено трассой оригинала): draw_tile_right, + * draw_tile_anim_right и draw_loose добавляют спрайты через add_backtable + * НАПРЯМУЮ, игнорируя ptr_add_table — значит правая грань левого соседа + * («шахматка» столба 93, blueline, грани пик/loose соседа, кадр loose) + * ВСЕГДА рисуется ПОД персонажем. Через ptr_add_table (→ midtable при + * draw_other_overlay) идут только: «floor B» 42 при левом соседе-поле, + * draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom. */ +static void overlay_mid_tile(int row, int col) +{ + uint8_t code = tile_code(row, col); + uint8_t lcode = tile_code(row, col - 1); + const piece *t; + int x = COL_XH[col] * 8; + int dby = 63 * row + 65; + int dmy = dby - 3; + if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } + else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } + if (lcode == 0x0F) { if ((pop_doorlink2(tile_mod(row, col - 1)) & 0x1F) > 1) lcode = 1; } + else if (lcode == 0x06) { if ((pop_doorlink2(tile_mod(row, col - 1)) & 0x1F) > 1) lcode = 5; } + t = &tile_table[code]; + ov_mark(x, 63 * row + 2, 64, 64); /* тайл + правый сосед (грани) */ + if (code != 20 && lcode == 1) /* draw_tile_right: только 42 */ + env_b(42, x, tile_table[1].right_y + dmy); + if (t->base_id) /* draw_tile_base (loose base = 0) */ + env_b(t->base_id, x, t->base_y + dmy); + if (code == 2) { /* draw_tile_anim: свои пики */ + uint8_t sm = tile_mod(row, col); + uint8_t sf = (sm & 0x80) ? 5 : sm; + if (sf > 0 && sf < 10 && SPIKES_FRAM_LEFT[sf]) + env_b(SPIKES_FRAM_LEFT[sf], x, dmy - 2); + } + if (code == 20) { /* draw_tile_bottom */ + uint8_t wmod = wall_modifier(row, col); + wall_b(WALL_FRAM_BOTTOM[wmod], x, dby); + wall_pattern(row, col, 0); + } else if (t->bottom_id) { + env_b(t->bottom_id, x, dby); + } +} + +/* draw_tile_fore (seg008:690) — ТОЛЬКО передний слой (add_foretable): он + * рисуется после всего и поверх персонажа. Нужен для полосы кладки у + * потолка: в redraw_needed_above всё остальное идёт в backtable (под Kid). */ + /* draw_other_overlay (seg008:1492): для поз виса/прыжка/падения/захвата на * КРОМКЕ (сосед слева пуст, либо тайл через один слева пуст) — перерисовать * ВЕСЬ тайл поверх Kid. Тело пола перекрывает висящего/прыгающего/падающего * ПОД ним. Срабатывает только в этих позах (см. redraw_at_char2 ниже) — - * поэтому при стоянии/залезании ложной окклюзии нет. */ + * поэтому при стоянии/залезании ложной окклюзии нет. + * + * ПОРЯДОК (redraw_needed_tiles, seg008:1B06 + redraw_needed, seg008:0211): + * тайлы обходятся ряды 2,1,0, внутри ряда колонки 0..9; в тайле сначала + * оверлей (redraw_frames2 -> midtable), потом ОБЪЕКТЫ этого тайла. Kid — тоже + * запись midtable, вставляемая при обработке СВОЕГО тайла (obj_row/obj_col). + * Значит оверлей ложится ПОВЕРХ Kid только у тайлов, идущих в обходе ПОЗЖЕ + * его собственного; всё, что раньше, — под ним. У нас списков нет (рисуем + * сразу), поэтому порядок эмулируется явной проверкой. */ static void other_overlay_tile(int row, int col) { uint8_t code = tile_code(row, col); @@ -802,9 +1047,19 @@ static void other_overlay_tile(int row, int col) * висящем/падающем Kid. Фон запечён один раз в pop_room_draw — над * спрайтом его перерисовывать не нужно. */ if (code == 0) return; + /* Тайл идёт в обходе РАНЬШЕ тайла Kid → его оверлей лёг бы ПОД спрайт. */ + if (row > obj_row || (row == obj_row && col <= obj_col)) return; left = tile_code(row, col - 1); - if (left == 0 || (col > 0 && tile_code(row, col - 2) == 0)) - draw_tile(row, col); + if (left == 0) { overlay_mid_tile(row, col); return; } /* ветка 1: сосед слева пуст */ + if (col > 0 && tile_code(row, col - 2) == 0) { + /* Ветка 2 (тайл ЧЕРЕЗ ОДИН слева пуст): в оригинале она ставит + * tile_object_redraw[tilepos]=0xFF, т.е. объекты тайлов tilepos−1 и + * tilepos рисуются СРАЗУ ПОСЛЕ оверлея — Kid возвращается наверх. + * Без этого правая грань соседнего столба (кадр 93) остаётся поверх + * прыгающего Kid (баг «тень на дальнем столбе поверх Кида»). */ + if (row == obj_row && (col == obj_col + 1)) return; + overlay_mid_tile(row, col); + } } void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, @@ -813,7 +1068,7 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, int wh = (int)((w + 1) >> 1); /* char_width_half */ int xl = obj_x / 2 + 58; /* char_x_left (58-база) */ int xr; - int8_t cL, cR, cLraw, rT, rB, r, c; + int8_t cL, cR, cLraw, rT, rB, r, c, rTraw; if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ xr = xl + wh; cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */ @@ -829,6 +1084,7 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, rT = y_to_row(top_y); rB = y_to_row(obj_y); if (rB == -1) rB = 3; /* обёртка → ряд 3 (ниже комнаты) */ + rTraw = rT; /* −1 = верх спрайта в полосе у потолка */ } /* single-room roomtest: ряд 3 (комната снизу) в этом виде не рисуется → * окклюдер спереди = ряд 2 (передняя грань пола, за которой нога). */ @@ -840,10 +1096,17 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, * (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим * включение ряда rB-1. */ if (rB > 0 && rT >= rB) rT = rB - 1; + /* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт + * Kid попадает в midtable — база порядка окклюзии в other_overlay_tile. + * run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/ + * подъём — колонка на одну ЛЕВЕЕ curr_col. */ + obj_row = Kid.curr_row; + obj_col = Kid.curr_col; + if (action == 1) { obj_row = rB; obj_col = cL; } + if ((frame >= 135 && frame < 149) || + action == 2 || action == 3 || action == 4 || action == 6) + obj_col--; gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, heal восстановит */ - for (r = rT; r <= rB; r++) - for (c = cL; c <= cR; c++) - fore_tile(r, c); /* Шовная окклюзия ворот (порт redraw_at_char для шва): футпринт зашёл за * ЛЕВЫЙ шов (cLraw<0) и у соседа слева в этом ряду решётка ворот (код 4) — * перерисовать её бары ПОВЕРХ Kid. Иначе Kid, стоя за закрытыми воротами @@ -879,5 +1142,25 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, } } } + /* draw_tile_fore (seg008:690) — foretable, рисуется ПОСЛЕ midtable, т.е. + * ПОСЛЕ оверлеев выше: передняя грань колонны перекрывает скос пола + * (floor_left_overlay), а не наоборот. Раньше fore шёл первым, и тёмный + * скос ложился поверх колонны — «тень у её основания». */ + for (r = rT; r <= rB; r++) + for (c = cL; c <= cR; c++) + fore_tile(r, c); + /* redraw_needed_above (seg008:0221): полоса кладки У ПОТОЛКА (ряд −1 = + * ряд 2 комнаты сверху). В оригинале она рисуется в самом конце обхода + * тайлов — ПОСЛЕ объектов, т.е. поверх персонажа (redraw_frames_above, + * куда set_redraw* уводит отрицательные tilepos). Без этого руки Kid, + * прыгающего в ряду 0, лезли поверх потолка. */ + if (rTraw < 0) { + g_clip_top = POP_YOFF; + for (c = cL; c <= cR; c++) { + ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H); + ceil_over_kid_tile(c); + } + g_clip_top = 0; + } gfx_set_bank(GFX_BANK_NORMAL); } diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 6f35739..f19b1e5 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -58,6 +58,12 @@ void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, int8_t dir, uint8_t frame, uint8_t action); +/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ + * (draw_other_overlay/floor_overlay/полоса потолка): он выходит за + * прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице + * дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */ +void pop_fore_heal(void); + /* Loose floors — динамическая отрисовка (состояние pop_loose_modif в * pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick. * shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту @@ -65,6 +71,16 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, void pop_loose_shake_draw(int row, int col); void pop_loose_bake_empty(int row, int col); +/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в + * полосе у потолка / «запечь» пустоту (колодец наверх) после провала. + * Состояние — pop_ceil_modif[10] в pop_map. */ +void pop_ceil_shake_draw(int col); +void pop_ceil_bake_empty(int col); + +/* Кусок приземлился (loose_land): 0 = нет, иначе tilepos+1 тайла, на который + * он лёг. Ставит pop_loose_mob_tick, разбирает pop_loose_tick (debris+knock). */ +extern uint8_t pop_loose_landed; + /* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому * modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */ void pop_spike_redraw(int row, int col); @@ -74,9 +90,11 @@ void pop_spike_redraw(int row, int col); * на back-странице, когда сосед слева анимируется. См. pop_bg.c. */ void pop_room_redraw_seam_left(void); -/* Кнопка (opener/closer) — перерисовать тайл (row,col) на back-странице: - * нажата/отжата по таймеру связи. Звать из pop_process_trobs. */ -void pop_button_redraw(int row, int col); +/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице + * (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте: + * кнопка нажата/отжата, floor → debris от упавшей плиты. */ +void pop_floor_bake(int row, int col); +void pop_button_redraw(int row, int col); /* = pop_floor_bake (кнопка) */ /* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла * (row,col) и покадровое продвижение+отрисовка на back-странице. tick @@ -85,6 +103,13 @@ void pop_loose_mob_spawn(int row, int col); void pop_loose_mob_tick(void); void pop_loose_mob_reset(void); /* остановить кусок (смена комнаты) */ +/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs, + * seg008:1561). Звать после kid_draw, перед fore-слоем. */ +void pop_loose_mob_draw_over(void); + +/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */ +uint8_t pop_loose_mob_pos(int *col, int *y); + /* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом * sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше * верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра. diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index 78e3b37..10bdc6e 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -297,6 +297,16 @@ static void control_jumpup(void) static void control(void) { uint8_t f = Kid.frame; + /* seg005 control(): в отскоке (bumped) и в свободном падении управление + * НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок + * прерывает присед после удара (medland/плита по голове) нажатием ↑, и + * последовательность не доигрывает свои dy(1)+dy(1), компенсирующие + * стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на + * пару пикселей меньше, чем должен»). */ + if (Kid.action == 5 || Kid.action == 4) { /* bumped / in_freefall */ + release_arrows(); + return; + } if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53)) control_standing(); else if (f == FRAME_48_TURN) diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 292fc39..098bd87 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -194,6 +194,58 @@ int kid_fp_obj_y(void) { return kid_fp_y; } uint16_t kid_fp_width(void) { return kid_fp_w; } uint16_t kid_fp_height(void) { return kid_fp_h; } +/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт + * chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27). + * Позиция от кадра Kid: obj_y −= 15, obj_x += 5 ВПЕРЁД (obj_dx_forward). + * Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают + * иначе — тут только общий случай + «на пиках» (порт по мере надобности). + * Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */ +#define KID_SPLASH_IMAGE 218 + +void kid_draw_splash(void) +{ + int obj_x = kid_fp_x, obj_y = kid_fp_y - 15; + uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7; + const uint8_t *img; + uint16_t w, h; + if (page >= kid_npages) return; + if (Kid.frame == 177) /* напоролся на пики */ + obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5; + else + obj_x += (Kid.direction < 0) ? -5 : 5; + obj_x = obj_x * 8 / 7; /* лог. X -> экран (calc_screen_x_coord) */ + img = (const uint8_t *)atlas_image(&kidp[page], idx); + gfx_w0_map(kidp[page].page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + if (w && h) { + uint8_t flip = (Kid.direction >= 0); + int bx = obj_x; + int top = obj_y - (int)h + 1; + if (flip) bx -= (int)w; + gfx_set_bank(GFX_BANK_SPRITE); + gfx_blit_cols(bx, top + POP_YOFF, img, flip); + gfx_set_bank(GFX_BANK_NORMAL); + pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); + /* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе + * брызги останутся на экране после кадра. */ + { + uint8_t dp = gfx_get_draw_page(); + if (kid_lvalid[dp]) { + int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx; + int y0 = kid_ly[dp] < top ? kid_ly[dp] : top; + int x1 = kid_lx[dp] + (int)kid_lw[dp]; + int y1 = kid_ly[dp] + (int)kid_lh[dp]; + if (bx + (int)w > x1) x1 = bx + (int)w; + if (top + (int)h > y1) y1 = top + (int)h; + kid_lx[dp] = x0; kid_ly[dp] = y0; + kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0); + } + } + } + gfx_w0_unmap(); +} + /* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */ void kid_heal(void) { diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index e982d9c..e33698d 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -68,6 +68,10 @@ void pop_kid_set_render_dx(int dx); * теневой копии; запоминает прямоугольник для kid_heal). */ void kid_draw(void); +/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid — + * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ +void kid_draw_splash(void); + /* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid — * вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */ void kid_heal(void); diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index a54c1d5..b58629d 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -101,6 +101,7 @@ uint8_t pop_leave_dir; /* уход вбок/вверх: 0=нет; 1=le uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заблокирован N кадров после входа в комнату */ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */ uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */ +uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */ uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */ @@ -114,6 +115,23 @@ static uint8_t grab_timer; /* после зацепа (12): блокиру static uint8_t g_link_l, g_link_r, g_link_u, g_link_d; static uint8_t g_lcol[3], g_rcol[3]; +/* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- * + * В оригинале это тот же loose, просто get_tile ушёл в комнату сверху + * (find_room_of_tile) и tilepos получился отрицательным (set_redraw* уводит + * такие в redraw_frames_above). У нас ряд 2 верхнего соседа живёт отдельной + * копией (above_fg в приложении, наш «ряд −1»), поэтому и состояние + * отдельное — pop_ceil_modif[col] с той же семантикой (0 покой, 0x80.. — + * тряска, 1..11 — отсчёт до падения). */ +uint8_t pop_ceil_modif[10]; /* публично: читает pop_bg при отрисовке */ +static uint8_t ceil_rest[10]; /* «перерисовать ПОКОЙ» (2 = обе страницы) */ +static uint8_t ceil_bake[10]; /* «запечь колодец» на второй странице */ +static uint8_t land_bake, land_bake_pos; /* щебень от приземления — вторая страница */ +static uint8_t *g_above; /* ряд 2 комнаты сверху (fg), мутабельный */ +uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провалившейся плиты-потолка */ +uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */ + +void pop_map_set_above(uint8_t *above_row2_fg) { g_above = above_row2_fg; } + void pop_map_set(uint8_t *fg) { g_fg = fg; } void pop_map_set_room(uint8_t room) { g_room = room; } @@ -138,7 +156,12 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, static uint8_t get_tile(int8_t col, int8_t row) { - if (row < 0 || row > 2) + /* Ряд −1 — ряд 2 комнаты СВЕРХУ (порт find_room_of_tile, seg006:005D: + * отрицательный row уходит по links.up). Нужен для зацепа за край + * провалившейся плиты-потолка (колодец наверх) и выбора seq прыжка. */ + if (row == -1 && g_above && g_link_u && col >= 0 && col <= 9) + g_curr_tile = (uint8_t)(g_above[col] & 0x1F); + else if (row < 0 || row > 2) g_curr_tile = TILE_WALL; else if (col == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */ g_curr_tile = g_lcol[row] & 0x1F; @@ -728,17 +751,22 @@ static void check_bumped(void) * dist_from_wall_forward/x_bump[] — мусор с большим отриц. сдвигом → * underflow X и уход в стену. fell_out ловит do_fall. */ if (Kid.curr_row < 0 || Kid.curr_row > 2) return; - t = get_tile_at_char(); tcol = Kid.curr_col; + /* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются + * (bump_col_* сбрасываются и функция выходит) — bump'а в развороте нет. */ + if (Kid.action == ACT_TURN) return; + /* Стена ищется по колонке, в которую упирается ПЕРЕДНИЙ КРАЙ персонажа + * (порт seg004: bump_col_left/right_of_wall — колонка, где край пересёк + * грань; НЕ curr_col!). Через curr_col было ДВА расхождения: (1) весовая + * точка кадра заезжает за грань раньше края, curr_col становился −1 на + * шве, ворота оказывались «под персонажем» и гард односторонней стены их + * снимал — Kid уходил в соседнюю комнату СКВОЗЬ ОПУЩЕННУЮ РЕШЁТКУ; + * (2) тот же гард был нужен только из-за отсутствия проверки разворота + * (см. выше). Колонка переднего края даёт и то, и другое даром: стоя + * СПРАВА от ворот лицом вправо, край в соседней колонке — ворота не + * бампят; заехав краем за их грань лицом влево — бампят. */ + tcol = get_tile_div_mod_m7(char_front_coll()); + t = get_tile(tcol, Kid.curr_row); wt = wall_type(t); - /* seg004: bump проверяется по колонке СБОКУ от стены (bump_col_left/right_ - * of_wall), а НЕ по тайлу ПОД персонажем. Стоя́ на односторонней стене - * (ворота = стена справа / зеркало = стена слева) лицом ПРОЧЬ от её - * блокирующей стороны — она не бампит. Иначе разворот на тайле ворот у - * шва выталкивает Kid к дальней грани ворот (сквозь закрытую решётку в - * соседнюю комнату — баг «телепорта» при развороте). */ - if ((wt == 1 && Kid.direction < 0) || (wt == 2 && Kid.direction >= 0)) - wt = 0; - if (!wt) { t = get_tile_infrontof_char(); tcol = g_infrontx; wt = wall_type(t); } if (!wt) return; if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row)) return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */ @@ -761,6 +789,8 @@ uint8_t pop_loose_modif[30]; /* публично: читает pop_bg п static uint8_t loose_bake[30]; /* счётчик «запечь пусто» (2 = обе страницы) */ static uint8_t loose_rest[30]; /* счётчик «перерисовать ПОКОЙ» после тряски (2 = обе стр.) */ +/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */ + /* make_loose_fall (seg007:0EF6): взвести отсчёт падения, если ещё не идёт. */ static void make_loose_fall(int pos, uint8_t modifier) { @@ -776,16 +806,37 @@ static void make_loose_fall(int pos, uint8_t modifier) static void check_press(void) { uint8_t frame = Kid.frame, action = Kid.action; - if (frame == 79) { /* прыжок вверх — тайл сверху */ - if (get_tile_above_char() == TILE_LOOSE) + uint8_t above_press = 0; /* «нажимается» тайл СВЕРХУ */ + /* Кадры виса (87..99) и начала подъёма (135..140): нажимается тайл, за + * КОТОРЫЙ ПЕРСОНАЖ ДЕРЖИТСЯ, т.е. верхний (seg006:1688). Отсюда падение + * плиты, за край которой Kid зацепился, — а не только той, по которой он + * ударил снизу прыжком (кадр 79). */ + if ((frame >= 87 && frame < 100) || (frame >= 135 && frame < 141)) { + above_press = 1; + } else if (action == ACT_BUMPED || action == ACT_TURN || action < ACT_HANGCLIMB) { + if (frame == 79) above_press = 1; /* прыжок вверх — удар снизу */ + } else { + return; /* seg006:1704 — прочее не жмёт */ + } + if (above_press) { + if (Kid.curr_row == 0) { + /* Ряд 0: «сверху» — ряд 2 комнаты СВЕРХУ (в оригинале get_tile с + * row=−1 уходит по links.up, find_room_of_tile seg006:005D). + * Плита-ПОТОЛОК; кнопок на потолке в этом тесте нет. */ + int8_t c = Kid.curr_col; + if (g_above && g_link_u && c >= 0 && c <= 9 && + (g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) && + (int8_t)pop_ceil_modif[c] <= 0) + pop_ceil_modif[c] = 1; /* make_loose_fall(1) */ + } else if (get_tile_above_char() == TILE_LOOSE) { make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1); + } return; } - /* стоя́ние: action turn/bumped или < hang_climb (0/1), с FRAME_NEEDS_FLOOR. - * ACT_TURN обязателен (SDLPoP check_press: actions_7_turn): при развороте - * на кнопке иначе trigger_button не зовётся кадр-два → таймер связи падает - * <2 → кнопка ложно отжимается и жмётся снова. */ - if (action != ACT_BUMPED && action != ACT_TURN && action >= ACT_HANGCLIMB) return; + /* Стояние: кадр обязан требовать пол; нажимается тайл ПОД персонажем. + * ACT_TURN обязателен (seg006:1691): при развороте на кнопке иначе + * trigger_button не зовётся кадр-два → таймер связи падает <2 → кнопка + * ложно отжимается и жмётся снова. */ if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return; { uint8_t t = get_tile_at_char(); @@ -804,6 +855,13 @@ static void check_press(void) static void do_knock(int tile_row) { int col, pos; + if (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */ + if (!g_above || !g_link_u) return; + for (col = 0; col < 10; col++) + if ((g_above[col] & 0x1F) == TILE_LOOSE && pop_ceil_modif[col] == 0) + pop_ceil_modif[col] = 0x80; + return; + } if (tile_row < 0 || tile_row > 2) return; for (col = 0; col < 10; col++) { pos = tile_row * 10 + col; @@ -835,9 +893,61 @@ void pop_loose_reset(void) loose_bake[i] = 0; loose_rest[i] = 0; } + for (i = 0; i < 10; i++) { + pop_ceil_modif[i] = 0; + ceil_rest[i] = 0; + ceil_bake[i] = 0; + } + pop_ceil_fell = 0; + pop_debris_at = 0; + pop_loose_landed = 0; + land_bake = 0; pop_loose_mob_reset(); } +/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге + * (кадры 5..14) не бьёт (кроме уровня 13 — его тут нет); в висе/подъёме тоже + * (action >= hang_climb, кроме turn). Иначе −1 HP: смерть → seq_22 (crushed), + * живой → seq_52 (плита придавила: Kid приседает), с отступом на 2 назад, + * если сзади пусто. Кадр 109 (присед) последовательность не меняет. */ +#define SEQ_22_CRUSHED 22 +#define SEQ_52_LOOSE_FELL_ON_KID 52 +#define FRAME_109_CROUCH 109 + +static void fell_on_your_head(void) +{ + uint8_t frame = Kid.frame, action = Kid.action; + if ((frame < 5 || frame >= 15) && + (action < ACT_HANGCLIMB || action == ACT_TURN)) { + Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; + pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */ + if (take_hp(1)) { + kid_set_seq(SEQ_22_CRUSHED); + play_seq(); + determine_col(); + pop_kid_dead = 1; + } else if (frame != FRAME_109_CROUCH) { + if (get_tile_behind_char() == TILE_EMPTY) + Kid.x = (uint8_t)char_dx_forward(-2); + kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID); + play_seq(); + determine_col(); + } + } +} + +/* check_loose_fall_on_kid (seg007:1192): падающий кусок в той же колонке и + * его низ уже в полосе 30px над ногами Kid → удар. */ +static void check_loose_fall_on_kid(void) +{ + int mcol, my; + if (pop_kid_dead) return; + if (!pop_loose_mob_pos(&mcol, &my)) return; + if (Kid.curr_col != (int8_t)mcol) return; + if (my < (int)Kid.y && (int)Kid.y - 30 < my) + fell_on_your_head(); +} + /* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ * (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */ void pop_loose_tick(void) @@ -888,7 +998,53 @@ void pop_loose_tick(void) } if (++col == 10) { col = 0; row++; } /* row/col без деления */ } + /* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры + * рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в + * НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */ + for (col = 0; col < 10; col++) { + if (ceil_rest[col]) { + pop_ceil_shake_draw(col); /* modif уже 0 → кадр покоя */ + ceil_rest[col]--; + } else if (pop_ceil_modif[col] != 0) { + uint8_t m = ++pop_ceil_modif[col]; + if (m & 0x80) { /* тряска от сотрясения (do_knock) */ + if (m >= 0x84) { pop_ceil_modif[col] = 0; ceil_rest[col] = 2; } + else pop_ceil_shake_draw(col); + } else if (m >= 11) { /* loose_floor_delay — плита рушится */ + pop_ceil_modif[col] = 0; + if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */ + pop_ceil_bake_empty(col); /* колодец наверх на ТЕКУЩЕЙ странице */ + ceil_bake[col] = 1; /* + вторая страница следующим кадром */ + pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */ + pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */ + } else { + pop_ceil_shake_draw(col); /* отсчёт 1..10 — дрожащий кадр */ + } + } else if (ceil_bake[col]) { + pop_ceil_bake_empty(col); + ceil_bake[col]--; + } + } pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */ + check_loose_fall_on_kid(); /* seg007:1192 — плита падает Киду на голову */ + if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */ + uint8_t pos = (uint8_t)(pop_loose_landed - 1); + uint8_t r = 0, c = pos; + pop_loose_landed = 0; + while (c >= 10) { c -= 10; r++; } + g_fg[pos] = TILE_DEBRIS; + /* Перерисовка ПО ПЛОЩАДИ ПОЛА (не bake_empty: та стирает и верх стены + * ряда ниже — оставался чёрный прямоугольник под тайлом). */ + pop_floor_bake(r, c); + land_bake_pos = pos; land_bake = 1; /* вторая страница дабл-буфера */ + do_knock(r); /* удар плиты трясёт loose этого ряда */ + pop_debris_at = (uint8_t)(pos + 1); /* сигнал приложению (персистентность) */ + } else if (land_bake) { + uint8_t r = 0, c = land_bake_pos; + while (c >= 10) { c -= 10; r++; } + pop_floor_bake(r, c); + land_bake = 0; + } } /* y_to_row_mod4 (seg006): ряд по y с оборотом mod4 (TILE_SIZEY=63). */ @@ -901,6 +1057,20 @@ static int8_t y_to_row(int16_t y) { return (int8_t)(((y + 60) / 63) % 4 - 1); } * решит: переход (links.down) или выпал из уровня (нет комнаты снизу). */ static void check_leave_below(void) { + /* leave_room (seg002:0504) проверяет ВВЕРХ ПЕРВЫМ: персонаж не в отскоке/ + * полёте и его y (как signed) в полосе −16..10 у верха комнаты — он вылез + * в комнату сверху (через колодец от упавшей плиты). Порядок важен: при + * подтягивании y ≈ 248, и проверка «ниже комнаты» (y>=211) иначе + * перехватывает подъём и роняет Kid из уровня. + * goto_other_room(up): y += 189, curr_row = y_to_row_mod4. */ + if (g_link_u && Kid.action != ACT_BUMPED && Kid.action != ACT_FREEFALL && + Kid.action != ACT_MIDAIR && + (int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) { + Kid.y = (uint8_t)(Kid.y + 189); + Kid.curr_row = y_to_row((int16_t)Kid.y); + pop_leave_dir = 3; + return; + } if (Kid.y >= 211) { Kid.y = (uint8_t)(Kid.y - 189); Kid.curr_row = y_to_row((int16_t)Kid.y); diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 3ebc0c3..036c5bf 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -60,6 +60,23 @@ void pop_loose_reset(void); /* сброс loose-состояния при см * Приложение: определить debris-приземление в комнате снизу. Снять (=0). */ extern uint8_t pop_loose_fell; +/* Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ (в оригинале — тот же loose с + * отрицательным tilepos, seg006 check_press frame 79 / seg007 animate_loose). + * pop_map_set_above даёт МУТАБЕЛЬНУЮ копию ряда 2 верхнего соседа (ту же, что + * ушла в pop_room_set_above): провалившаяся плита становится в ней EMPTY — + * сверху открывается колодец. pop_ceil_modif[col] — состояние (как + * pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности + * между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты, + * pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */ +void pop_map_set_above(uint8_t *above_row2_fg); +extern uint8_t pop_ceil_modif[10]; +extern uint8_t pop_ceil_fell; +extern uint8_t pop_debris_at; + +/* Взводится в кадре, когда Kid получил урон (hitp_delta<0 оригинала) — + * приложение рисует «брызги» kid_draw_splash() поверх Kid и снимает флаг. */ +extern uint8_t pop_kid_hurt; + /* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда * приземления/удара. Зовётся из pop_phys_tick. */ void pop_check_knock(void); diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 897f87a..3cdada6 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -27,7 +27,7 @@ __sfr __at (0xFE) io_border; * row2, шахта col3). Kid на полу row0 col1, идёт вправо → падает в шахту → * пики. TODO: вернуть на pop_level_start_room (комната 1) после отладки. */ #define START_ROOM 6 -#define START_COL 1 +#define START_COL 4 #define START_ROW 0 #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ @@ -67,34 +67,47 @@ static uint8_t seam_redraw; /* оставшиеся кадры редроя ( static uint8_t room_label_sig = 0xFF; static uint8_t room_label_redraw; -/* Персистентные debris-переопределения (упавший loose-кусок приземлился в - * комнате снизу → floor стал debris). Минимальный per-room override: живёт - * между переходами, применяется в enter_room поверх загруженных тайлов. - * (Полноценное per-room modifier-состояние — в docs/gates_spikes_plan.md, P0.) */ +/* Персистентные переопределения тайлов уровня: упавший loose-кусок оставил + * щебень (floor → debris), а на месте самой плиты — пусто (в т.ч. в комнате + * СВЕРХУ: провалившийся потолок = колодец). Минимальный per-room override: + * живёт между переходами, применяется в enter_room поверх загруженных тайлов + * (и для ряда 2 верхнего соседа). (Полноценное per-room modifier-состояние — + * в docs/gates_spikes_plan.md, P0.) */ #define TILE_DEBRIS 0x0E -#define DEBRIS_OVR_MAX 4 -static uint8_t dbr_room[DEBRIS_OVR_MAX], dbr_pos[DEBRIS_OVR_MAX], dbr_cnt; +#define TILE_EMPTY 0x00 +#define OVR_MAX 8 +static uint8_t ovr_room[OVR_MAX], ovr_pos[OVR_MAX], ovr_tile[OVR_MAX], ovr_cnt; -static void add_debris_override(uint8_t room, uint8_t pos) +static void add_tile_override(uint8_t room, uint8_t pos, uint8_t tile) { uint8_t i; - for (i = 0; i < dbr_cnt; i++) - if (dbr_room[i] == room && dbr_pos[i] == pos) return; /* уже есть */ - if (dbr_cnt < DEBRIS_OVR_MAX) { - dbr_room[dbr_cnt] = room; dbr_pos[dbr_cnt] = pos; dbr_cnt++; + for (i = 0; i < ovr_cnt; i++) + if (ovr_room[i] == room && ovr_pos[i] == pos) { ovr_tile[i] = tile; return; } + if (ovr_cnt < OVR_MAX) { + ovr_room[ovr_cnt] = room; ovr_pos[ovr_cnt] = pos; + ovr_tile[ovr_cnt] = tile; ovr_cnt++; } } +/* Применить переопределения комнаты room к массиву тайлов: base = tilepos + * первого элемента (0 для полной комнаты, 20 для ряда 2 верхнего соседа). */ +static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t n) +{ + uint8_t i; + for (i = 0; i < ovr_cnt; i++) + if (ovr_room[i] == room && ovr_pos[i] >= base && ovr_pos[i] < base + n) + tiles[ovr_pos[i] - base] = ovr_tile[i]; +} + /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */ static void enter_room(uint8_t room) { - uint8_t pg, i; + uint8_t pg; uint8_t *room_mod; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); - for (i = 0; i < dbr_cnt; i++) /* применить debris-переопределения */ - if (dbr_room[i] == room) room_fg[dbr_pos[i]] = TILE_DEBRIS; + apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */ pop_map_set(room_fg); pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), pop_room_link(room, 2), pop_room_link(room, 3), @@ -109,10 +122,13 @@ static void enter_room(uint8_t room) pop_level_access_begin(); for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c)); pop_level_access_end(); + apply_overrides(up, above_fg, 20, 10); /* провалившийся потолок = колодец */ for (c = 0; c < 10; c++) above_mod[c] = um[20 + c]; pop_room_set_above(above_fg, above_mod); + pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */ } else { pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */ + pop_map_set_above(0); } } pop_map_set_room(room); @@ -232,6 +248,7 @@ int main(void) pop_ctrl_tick(); /* ввод -> control(): смена seq */ kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ + pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */ kid_tick(); /* play_seq: следующий кадр */ pop_phys_tick(); /* K3: падение/приземление/стена */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ @@ -295,18 +312,35 @@ int main(void) } io_border = 6; /* голубой */ if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */ - uint8_t fcol = (uint8_t)((pop_loose_fell - 1) % 10); + 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; io_border = 2; /* синий */ + add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */ if (down) { int8_t lrow = pop_room_col_landing(down, fcol); - if (lrow >= 0) add_debris_override(down, (uint8_t)(lrow * 10 + fcol)); + if (lrow >= 0) add_tile_override(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS); } } + if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */ + uint8_t ccol = (uint8_t)(pop_ceil_fell - 1); + uint8_t up = pop_room_link(cur_room, 2); + pop_ceil_fell = 0; + if (up) add_tile_override(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */ + } + if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */ + add_tile_override(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS); + pop_debris_at = 0; + } io_border = 4; /* зеленый */ update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ + pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */ + if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ + kid_draw_splash(); + pop_kid_hurt = 0; + } io_border = 0; /* черный */ pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), kid_fp_width(), kid_fp_height(), @@ -324,15 +358,15 @@ int main(void) * а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */ { uint8_t r = cur_room; - if (r != room_label_sig) { room_label_sig = r; room_label_redraw = 2; } - if (room_label_redraw) { - char roomLabel[] = "Room ##"; - roomLabel[5] = (char)(r / 10 + '0'); - roomLabel[6] = (char)(r % 10 + '0'); - setcolor(WHITE); - outtextxy(100, 0, roomLabel); - room_label_redraw--; - } +// if (r != room_label_sig) { room_label_sig = r; room_label_redraw = 2; } +// if (room_label_redraw) { +// char roomLabel[] = "Room ##"; +// roomLabel[5] = (char)(r / 10 + '0'); +// roomLabel[6] = (char)(r % 10 + '0'); +// setcolor(WHITE); +// outtextxy(100, 0, roomLabel); +// room_label_redraw--; +// } } gfx_wait_vsync();