/* * pop_cdraw.h — ОТРИСОВКА ПЕРСОНАЖА, одна на всех Char. * * В оригинале это два входа с ИДЕНТИЧНЫМ телом (seg008:22F0/2324): * * add_kid_to_objtable add_guard_to_objtable * loadkid() loadshad() * load_fram_det_col() load_fram_det_col() * load_frame_to_obj() load_frame_to_obj() * stuck_lower() stuck_lower() * set_char_collision() set_char_collision() * set_objtile_at_char() set_objtile_at_char() * redraw_at_char() redraw_at_char() * redraw_at_char2() redraw_at_char2() * clip_char() clip_char() * add_objtable(0) add_objtable(1 тень / 2 страж) * * — различаются ТОЛЬКО окном (loadkid/loadshad), набором спрайтов и типом * объекта. У нас до DRAW-CHAR это были два независимых куска кода * (kid_draw в резиденте и pop_guard_draw в банке), и расхождение уже стоило * бага: падающий скелет рисовался поверх верхней грани пола соседней * колонки, потому что порт redraw_at_char2 был только у Кида. Тем же путём * пошли бы тень (ур. 4/5/6/12), визирь (13) и толстяк (12). * * Поэтому здесь ОДИН набор функций, а различия сведены к слоту: * POP_CH_KID — Кид: окно loadkid, атласы chtab_2 (kidp), кадр kid_frame; * POP_CH_OPP — соперник: окно loadshad, атласы chtab_5 (страж/скелет), * кадр pop_gframe. * * Порядок в кадре (задаёт главный цикл, как draw_people): * pop_char_heal(who) — стереть прошлый кадр ЭТОЙ страницы дабл-буфера; * pop_char_draw(who) — спрайт + брызги урона + клинок; * pop_char_fore(who) — передние грани и оверлеи тайлов ПОВЕРХ него. */ #ifndef POP_CDRAW_H #define POP_CDRAW_H #include /* Нижняя статус-полоса: строка делений HP. Живёт здесь, а не в pop_cdraw.c, * потому что её делит UI — «GAME PAUSED» в оригинале пишется в ту же полосу * (SDLPoP rect_bottom_text = {193,70,202,250}), между делениями Кида слева и * стража справа. */ #define POP_HP_Y 194 #define POP_CH_KID 0 #define POP_CH_OPP 1 #define POP_CH_N 2 /* Состояние отрисовки одного слота. Публичное, потому что метрики кадра * (fp*) читает физика из ДРУГОГО банка (char_width_half в * set_char_collision, порядок «кто поверх кого» в pop_bg): данные лежат в * _DATA (W2) и видны всем без трамплина, а вызов аксессора из банка стоил * бы дороже самого чтения. */ typedef struct { /* прямоугольник спрайта по СТРАНИЦАМ дабл-буфера: у каждой своя * видео-ОЗУ и своя ОЗУ-копия, поэтому heal обязан стирать спрайт именно * той страницы, в которую сейчас рисуем */ int x[2], y[2]; uint8_t w[2], h[2]; /* габарит: спрайты игры не крупнее 64x64 */ uint8_t valid[2]; /* НАКЛАДНЫЕ спрайты (клинок + брызги урона) — свой прямоугольник, не * объединение с персонажем: объединение сильно больше суммы двух * (клинок уходит вперёд-вверх), а heal стоит ровно по площади */ int ox[2], oy[2]; uint8_t ow[2], oh[2]; uint8_t ovalid[2]; /* габарит кадра для fore-прохода: fpx — ЛОГИЧЕСКАЯ X (до ×8/7), * fpy — низ спрайта; fpw == 0 — в этом кадре рисовать было нечего */ int fpx, fpy; uint8_t fpw, fph; /* окно fore-клипа слота (объединение «спрайт + накладные»), КОМНАТНЫЙ y */ int cx, cy; uint8_t cw, ch; /* straddle: рендерное смещение по ЛОГИЧЕСКОЙ X, когда комната персонажа * не совпадает с отрисованной (порт xpos_in_drawn_room) */ int render_dx; } pop_cdraw_t; extern pop_cdraw_t pop_cd[POP_CH_N]; /* Забыть, что нарисовано на обеих страницах: после меню, QuickLoad и любой * другой перерисовки экрана мимо кадрового конвейера. Без этого пропуск * перерисовки «тихого» персонажа оставляет его невидимым. */ void pop_cd_forget(void) __banked; /* Стереть прошлый кадр персонажа (спрайт + накладные) на ТЕКУЩЕЙ странице. */ void pop_char_heal(uint8_t who) __banked; /* Нарисовать текущий кадр: спрайт, брызги урона (по флагу pop_kid_hurt / * pop_guard_hurt) и клинок. Загружает окно Char сам — clip_char и футпринт * работают с АКТИВНЫМ персонажем, как в оригинале. */ void pop_char_draw(uint8_t who) __banked; /* Передние грани и оверлеи тайлов ПОВЕРХ персонажа: ставит окно fore-клипа * слота и зовёт общий проход pop_fore_over_char. Отдельным вызовом, а не * хвостом pop_char_draw, потому что порядок в кадре задаёт главный цикл: * между спрайтом и его fore-слоем успевает нарисоваться падающая плита. */ void pop_char_fore(uint8_t who) __banked; /* Straddle-смещение спрайта (см. render_dx). */ void pop_char_set_render_dx(uint8_t who, int dx) __banked; /* ---- Пропуск НЕИЗМЕНИВШЕГОСЯ кадра (DRAW-COST) ---------------------- * * heal + спрайт + fore-проход стоят ~50 % кадрового периода НА ПЕРСОНАЖА * (замер пользователя 2026-08-08: комната 1.3 с телом убитого стража — 210 % * кадра, соседняя 1.2 без тела — 140 %). Платили мы это и за то, что не * менялось: труп лежит неподвижно до выхода из комнаты, стоящий Кид — пока * игрок не нажал клавишу, а перерисовывались они каждый кадр. * * Оптимизация: у каждой страницы дабл-буфера свой снимок ВХОДОВ отрисовки * слота (поза, координаты, направление, меч, брызги, straddle). Если снимок * совпал, спрайт этой страницы не стёрт и фона под ним никто не трогал — * нужный кадр на странице УЖЕ нарисован, и heal/draw/fore пропускаются * целиком. * * `pop_cd_touch` — «фон трогали ВОТ ЗДЕСЬ»: слой фона копит ПРЯМОУГОЛЬНИК * своих правок по страницам дабл-буфера, и слот перерисовывается, только * если тот задел его самого. Флага «трогали хоть что-то» не хватило: * факелы анимируются КАЖДЫЙ кадр, и один факел в комнате обнулял пропуск * для всех персонажей сразу (замер 2026-08-08: комната 1.1, Кид стоит — * 597 684 такта, то есть ровно как без оптимизации). * * Метку ставит сам блит фона (`pop_blit_b`, по признаку «идёт не * fore-проход») плюс те немногие места, что стирают фон голым `bar` или * своим heal (падающий кусок, оверлей fore-heal). Координаты — ЭКРАННЫЕ * (с POP_YOFF). Прямоугольник копится сразу для ОБЕИХ страниц: правку фона * почти всегда повторяют на второй, а лишняя перерисовка персонажа дешевле * лишнего состояния. Снимает метку главный цикл — после того, как * персонажи на этой странице отрисованы. * * Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья, * а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */ extern uint8_t pop_cd_dirty; /* биты страниц: метка взведена */ /* [страница][колонка] = диапазон затронутых ЭКРАННЫХ y; пусто = ymin 255, * ymax 0. Раньше тут была маска колонок на три ряда по 63 px — она * склеивала касания внутри ряда и заставляла перерисовывать нетронутого * персонажа (разбор в шапке pop_cd_touch, pop_tile.c). */ extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10]; /* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то, * что трогали на странице p. Резидент (pop_tile.c). */ uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked; /* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd * (см. тело — там про цену пяти аргументов). */ uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked; void pop_cd_clear(uint8_t p) __banked; /* снять метку страницы целиком */ void pop_cd_init(void) __banked; /* пустые диапазоны на обеих страницах */ void pop_cd_touch(int x, int y, int w, int h); #define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256) /* Пакетная пометка: между begin и end блиты КОПЯТ общий прямоугольник, и * настоящая пометка ставится одна (на весь тайл), а не на каждый кусок. * Скобку ставит draw_tile — выигрыш ~15% цены блита фона. Вкладывать * скобки нельзя: аккумулятор один. */ extern uint8_t pop_cd_batch; void pop_cd_batch_begin(void); void pop_cd_batch_end(void); /* ПОДАВИТЬ пометки на время серии блитов — для того, кто пометил область САМ * и заведомо шире, чем нарисует (падающий кусок: pop_loose_mob_tick помечает * весь коридор heal'а одним вызовом, а спрайт куска — три блита внутри него). * Дешевле пакета: pop_cd_touch выходит второй проверкой, не считая ни колонок, * ни рядов. Замер 2026-08-17: настоящий вызов pop_cd_touch = 4 502 такта, * 28% всей цены блита. * * ОПАСНО применять без разбора: пропущенная пометка = персонаж, которого * стёрли, но не попросили перерисоваться, то есть исчезнувший со экрана * персонаж (ровно этот баг ловили 2026-08-13 в комнате 23). Ставить только * там, где покрытие доказано. */ void pop_cd_mute(void); void pop_cd_unmute(void); /* Кого в этом кадре можно НЕ перерисовывать: бит 0 — Кид, бит 1 — соперник. * Звать ДВАЖДЫ: перед heal (стирать нечего) и перед отрисовкой (тик мог * персонажа всё-таки сдвинуть — тогда бит снимется сам, а pop_char_draw * сотрёт прошлый кадр за нас). */ uint8_t pop_char_skip_mask(void) __banked; /* ОТРАЖЕНИЕ В ЗЕРКАЛЕ (уровень 4, seg003:0798). Char к моменту вызова уже * отражён (pop_check_mirror); clip_top — линия y_clip[curr_row+1], выше * которой отражение не рисуется. Отдельно от pop_char_draw, потому что и в * оригинале это сокращённый путь add_objtable(4): без клинка, брызг, * fore-прохода и пропуска кадра. heal — парный вызов, рядом с * pop_char_heal. */ void pop_mirror_draw(int clip_top) __banked; void pop_mirror_heal(void) __banked; #endif