/* * pop_cdraw.c — ОТРИСОВКА ПЕРСОНАЖЕЙ (Кид, страж, скелет; дальше тень, * визирь, толстяк) + полоса HP. Банк 4. * * Одно тело на всех Char — порт add_kid_to_objtable/add_guard_to_objtable * (seg008:22F0/2324), различия сведены к слоту; мотивация и порядок вызовов * в кадре — в шапке pop_cdraw.h. До DRAW-CHAR это были kid_draw в * резиденте и pop_guard_draw здесь же, каждый со своей математикой кадра, * своими heal-прямоугольниками и своим проходом окклюзии. * * Почему банк, а не резидент: зовёт только главный цикл (раз в кадр на * персонажа), горячих банк→банк переходов отсюда нет, а резидентное * пространство W1+W2 делят код и данные — килобайт кода, уехавший в банк, * это килобайт для кучи. Писучие статики банкового модуля (--dataseg не * задаём) остаются в обычном _DATA, то есть в W2: pop_cd читают и физика, и * фон из СВОИХ банков — без трамплина, обычной адресацией. */ #include #include #include #include "pop_arc.h" #include "guard_arc.h" #include "skel_arc.h" #include "vizier_arc.h" #include "pop_guard.h" #include "pop_kid.h" #include "_pop_kdraw.h" #include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */ #include "pop_cdraw.h" #include "pop_status.h" #include "pop_ui.h" /* POP_COL_OUTSIDE — фон вне игрового поля */ #include "pop_shadow.h" /* запечённый набор Тени */ #include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_state.h" /* ВРЕМЕННО: зонды pop_dbg_b* */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_map.h" /* hitp_curr/hitp_max — HP Кида; clip_char */ #include "pop_level.h" /* тип соперника уровня (набор спрайтов) */ #include "pop_tile.h" /* pop_heal_off — клип тени/отражения через фон */ #include "kid_data.h" #include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */ /* Последний curr_guard_color обычного стража (0 — цвет ещё не выбирали). * Нужен pop_guard_pal_apply, чтобы восстановить слоты после kid.pal. */ static uint8_t guard_pal_cur; /* 16 слотов POP_GUARD_PAL_BASE в ОБЕ палитры дабл-буфера. * Таблицы — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а * gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS читает * только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем запись в * локальный буфер — стек гарантированно в W2. Без копии BIOS забирал * мусор с чужой страницы и страж становился невидимым. */ static void pal_load16(const uint8_t *src) { uint8_t buf[64]; uint8_t i; for (i = 0; i < 64; i++) buf[i] = src[i]; gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); } /* Цвет стража (set_chtab_palette, seg003:257). Оригинал держит ОДИН набор * спрайтов и подменяет 16 цветов палитры chtab_5 по level.guards_color — * так на уровне 2 стражи комнат 4/7/11/15/24 разного цвета (1/3/1/1/6). * Полоса HP стража рисуется тем же атласом, поэтому меняется заодно. * color = 0 — палитру не трогаем (оригинал в этом случае set_chtab_palette * не зовёт: тип уровня не «обычный страж»). * Грузим В ОБЕ палитры: у страниц дабл-буфера они раздельные. */ void pop_guard_set_palette(uint8_t color) __banked { if (color == 0 || color > POP_GUARD_NPAL) return; guard_pal_cur = color; /* curr_guard_color, для re-apply */ pal_load16(pop_guard_pal[color - 1]); } /* Пере-применить палитру ТЕКУЩЕГО набора соперника — пара к * pop_bg_pal_apply/pop_shadow_pal_apply. Слоты 0x90..0x9F динамические: * общий kid.pal содержит все 256 записей и затирает их палитрой обычного * стража (цвет 2, тот что запёк упаковщик). Поэтому КАЖДЫЙ file-load * логической палитры обязан доплатить этот диапазон, иначе скелет (и * Джафар) остаются в цвете стража: у скелета все пиксели индекса 3, а это * слот 0x93 = розовый цвет стража — ровно то, что было видно на уровне 3. * Звать из pop_pal_* ДО snapshot. */ void pop_guard_pal_apply(void) __banked { int8_t type = pop_level_guard_type(pop_current_level); if (type == 2 || type == 3) { /* свой цвет — из файла набора */ pal_load16((type == 2) ? pop_skel_pal : pop_vizier_pal); } else if (type == 0 && guard_pal_cur != 0) { pal_load16(pop_guard_pal[guard_pal_cur - 1]); } } /* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */ #define GUARD_PAGES 5 static atlas_t gp[GUARD_PAGES]; static uint8_t g_ok; /* Состояние отрисовки по слотам (см. pop_cdraw.h): heal-прямоугольники по * страницам дабл-буфера, габарит кадра и окно fore-клипа. */ pop_cdraw_t pop_cd[POP_CH_N]; /* «Брызги» урона Кида — спрайт chtab_2 id 218 (res619.png, в наших атласах * страница 27). У соперника это image 1 его собственного набора. */ #define KID_SPLASH_IMAGE 218 /* Набор спрайтов соперника выбирает ТИП УРОВНЯ (load_lev_spr, seg000:1092): * tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, −1 нет}. * У нас пока два набора: обычный страж (уровни 1-2) и скелет (уровень 3); * у скелета и кадров меньше (28 против 34), поэтому пятую страницу он не * грузит. Уровень БЕЗ соперников (type < 0) не грузит ничего. */ int pop_guard_load(void) __banked { uint8_t tbl[ARC_GUARD_COUNT * POP_ARC_REC]; /* самый крупный из трёх */ pop_arc_t arc; int8_t type = pop_level_guard_type(pop_current_level); uint8_t i, pages, count, id; if (type < 0) return 0; /* соперников на уровне нет */ /* Наборы разложены по своим архивам, а не в один: имена внутри у них * совпадают (g0.atl у всех троих), да и грузится всегда ровно один. */ if (type == 2) { id = POP_ARC_SKEL; count = ARC_SKEL_COUNT; pages = ARC_SKEL_COUNT; } else if (type == 3) { id = POP_ARC_VIZIER; count = ARC_VIZIER_COUNT; pages = ARC_VIZIER_COUNT; } else { id = POP_ARC_GUARD; count = ARC_GUARD_COUNT; pages = GUARD_PAGES; } if (pages > GUARD_PAGES) pages = GUARD_PAGES; if (pop_arc_open_id(&arc, id, tbl, count) < 0) return -1; for (i = 0; i < pages; i++) if (pop_arc_atlas(&arc, tbl, i, &gp[i]) != 0) { pop_arc_close(&arc); return -1; } pop_arc_close(&arc); g_ok = 1; /* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется * из res10.bin по guards_color — это делает pop_guard_set_palette при * входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ pop_guard_pal_apply(); return 0; } /* Зеркальный набор соперника — пара к pop_guard_load: те же страницы, но * файлы *_v.atl (см. pop_vflip.h). Здесь, а не в загрузчике набора, * потому что gp[] и выбор GUARD/SKEL живут именно тут. */ int pop_guard_vflip_load(void) __banked { uint8_t tbl[ARC_GUARD_COUNT * POP_ARC_REC]; pop_arc_t arc; int8_t type = pop_level_guard_type(pop_current_level); uint8_t i; int res = 0; if (type < 0 || !g_ok) return 0; /* соперников на уровне нет */ /* Зеркала есть ТОЛЬКО у обычного стража: зелье инверсии — уровень 9, а * там tbl_guard_type = 0 (у скелета и Джафара своих *_v нет вовсе). */ if (type != 0 && type != 1) return 0; if (pop_arc_open_id(&arc, POP_ARC_GUARD, tbl, ARC_GUARD_COUNT) < 0) return -1; for (i = 0; i < GUARD_PAGES; i++) { atlas_t m; if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_GUARD_G0_V + i), &m) != 0 || pop_vflip_take(&gp[i], &m) != 0) { res = -1; break; } } pop_arc_close(&arc); return res; } void pop_guard_free(void) __banked { uint8_t i; if (!g_ok) return; for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]); g_ok = 0; } /* ---- calc_screen_x_coord (seg008:2602) БЕЗ деления ------------------- * * * Логическая X (280 шир = 28px/тайл) -> экранная (320 = 32px/тайл): ×8/7. * SDLPoP пишет это как `logical_x*320/280`, у нас SDCC разворачивал в * `__divsint` — 2 400 тактов на вызов, и зовётся оно на КАЖДОГО персонажа * КАЖДЫЙ кадр (последнее деление, остававшееся в горячем пути). * * ХРАНИМ x/7 БАЙТОМ, а не готовое x*8/7 словом. Тождество: 8x/7 == x + x/7 * (7x делится нацело, поэтому усечение обеих частей совпадает — и для * отрицательных с трункейтом к нулю тоже). Оба варианта собраны и * посчитаны по сгенерированному asm: * * int16_t готовое: ... add hl,hl / add hl,de / ld e,(hl) / inc hl / * ld d,(hl) = 132 такта, 2 304 байта * int8_t x/7: ... add hl,bc / ld a,(hl) / ld l,a / rlca / sbc a,a / * ld h,a / add hl,de = 131 такт, 1 152 байта * * Расширение знака и 16-битное сложение стоят РОВНО столько же, сколько * лишний `add hl,hl` при двухбайтном индексе, а чтение из памяти на одно * меньше — под wait-state'ами Sprinter (такт ≈ 2,4× номинала на обращениях * к ОЗУ) байтовый вариант даже чуть выгоднее номинала. Плюс килобайт * банка, который в pop_cdraw ещё понадобится. * * ДИАПАЗОН — весь, а не «обычный случай». Отрицательные X реальны: * obj_x = 2*fwd − 116 уходит в минус, как только fwd < 58 (левее * x_bump[5]), то есть у левой кромки комнаты — это не экзотика. По * фактическим данным кадров (kid.ani: dx Кида −5..+10, стража * −2..+10) при x типа uint8_t и render_dx ∈ {−140, 0, +140} полный * диапазон obj_x = −416..695; таблица кроет −448..703 с запасом. * Деление ниже поэтому недостижимо и оставлено страховкой на случай * третьего персонажа или другого render_dx. * * SCRX7[x + SCRX_BIAS] == x/7 (усечение к нулю, как в C и как у 8086 idiv * в оригинале); экранная X = x + SCRX7[...]. */ #define SCRX_BIAS 448 #define SCRX_N 1152 static const int8_t SCRX7[SCRX_N] = { -64, -63, -63, -63, -63, -63, -63, -63, -62, -62, -62, -62, -62, -62, -62, -61, -61, -61, -61, -61, -61, -61, -60, -60, -60, -60, -60, -60, -60, -59, -59, -59, -59, -59, -59, -59, -58, -58, -58, -58, -58, -58, -58, -57, -57, -57, -57, -57, -57, -57, -56, -56, -56, -56, -56, -56, -56, -55, -55, -55, -55, -55, -55, -55, -54, -54, -54, -54, -54, -54, -54, -53, -53, -53, -53, -53, -53, -53, -52, -52, -52, -52, -52, -52, -52, -51, -51, -51, -51, -51, -51, -51, -50, -50, -50, -50, -50, -50, -50, -49, -49, -49, -49, -49, -49, -49, -48, -48, -48, -48, -48, -48, -48, -47, -47, -47, -47, -47, -47, -47, -46, -46, -46, -46, -46, -46, -46, -45, -45, -45, -45, -45, -45, -45, -44, -44, -44, -44, -44, -44, -44, -43, -43, -43, -43, -43, -43, -43, -42, -42, -42, -42, -42, -42, -42, -41, -41, -41, -41, -41, -41, -41, -40, -40, -40, -40, -40, -40, -40, -39, -39, -39, -39, -39, -39, -39, -38, -38, -38, -38, -38, -38, -38, -37, -37, -37, -37, -37, -37, -37, -36, -36, -36, -36, -36, -36, -36, -35, -35, -35, -35, -35, -35, -35, -34, -34, -34, -34, -34, -34, -34, -33, -33, -33, -33, -33, -33, -33, -32, -32, -32, -32, -32, -32, -32, -31, -31, -31, -31, -31, -31, -31, -30, -30, -30, -30, -30, -30, -30, -29, -29, -29, -29, -29, -29, -29, -28, -28, -28, -28, -28, -28, -28, -27, -27, -27, -27, -27, -27, -27, -26, -26, -26, -26, -26, -26, -26, -25, -25, -25, -25, -25, -25, -25, -24, -24, -24, -24, -24, -24, -24, -23, -23, -23, -23, -23, -23, -23, -22, -22, -22, -22, -22, -22, -22, -21, -21, -21, -21, -21, -21, -21, -20, -20, -20, -20, -20, -20, -20, -19, -19, -19, -19, -19, -19, -19, -18, -18, -18, -18, -18, -18, -18, -17, -17, -17, -17, -17, -17, -17, -16, -16, -16, -16, -16, -16, -16, -15, -15, -15, -15, -15, -15, -15, -14, -14, -14, -14, -14, -14, -14, -13, -13, -13, -13, -13, -13, -13, -12, -12, -12, -12, -12, -12, -12, -11, -11, -11, -11, -11, -11, -11, -10, -10, -10, -10, -10, -10, -10, -9, -9, -9, -9, -9, -9, -9, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 49, 50, 50, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 68, 68, 68, 68, 68, 68, 68, 69, 69, 69, 69, 69, 69, 69, 70, 70, 70, 70, 70, 70, 70, 71, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 72, 73, 73, 73, 73, 73, 73, 73, 74, 74, 74, 74, 74, 74, 74, 75, 75, 75, 75, 75, 75, 75, 76, 76, 76, 76, 76, 76, 76, 77, 77, 77, 77, 77, 77, 77, 78, 78, 78, 78, 78, 78, 78, 79, 79, 79, 79, 79, 79, 79, 80, 80, 80, 80, 80, 80, 80, 81, 81, 81, 81, 81, 81, 81, 82, 82, 82, 82, 82, 82, 82, 83, 83, 83, 83, 83, 83, 83, 84, 84, 84, 84, 84, 84, 84, 85, 85, 85, 85, 85, 85, 85, 86, 86, 86, 86, 86, 86, 86, 87, 87, 87, 87, 87, 87, 87, 88, 88, 88, 88, 88, 88, 88, 89, 89, 89, 89, 89, 89, 89, 90, 90, 90, 90, 90, 90, 90, 91, 91, 91, 91, 91, 91, 91, 92, 92, 92, 92, 92, 92, 92, 93, 93, 93, 93, 93, 93, 93, 94, 94, 94, 94, 94, 94, 94, 95, 95, 95, 95, 95, 95, 95, 96, 96, 96, 96, 96, 96, 96, 97, 97, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 98, 98, 99, 99, 99, 99, 99, 99, 99, 100, 100, 100, 100, }; static int scr_x(int x) { /* Индекс считаем в uint16_t ОТДЕЛЬНОЙ переменной, а не выражением внутри * скобок: на `(const T*)КОНСТ + var*K` SDCC z80 умеет терять старший байт * индекса (memory sdcc_z80_const_ptr_index_bug), а тут индекс как раз * больше 255. */ uint16_t i = (uint16_t)(x + SCRX_BIAS); if (i < SCRX_N) return x + SCRX7[i]; return x * 8 / 7; /* по данным кадров недостижимо */ } /* Добавить накладной спрайт (клинок / брызги) в overlay-прямоугольник * страницы. Оба мелкие и в одном кадре встречаются редко — их объединение * дешевле третьего heal-слота. */ static void cd_overlay_add(pop_cdraw_t *s, uint8_t dp, int x, int y, uint8_t w, uint8_t h) { if (!s->ovalid[dp]) { s->ox[dp] = x; s->oy[dp] = y; s->ow[dp] = w; s->oh[dp] = h; s->ovalid[dp] = 1; return; } { int x0 = s->ox[dp], y0 = s->oy[dp]; int x1 = x0 + (int)s->ow[dp], y1 = y0 + (int)s->oh[dp]; if (x < x0) x0 = x; if (y < y0) y0 = y; if (x + (int)w > x1) x1 = x + (int)w; if (y + (int)h > y1) y1 = y + (int)h; s->ox[dp] = x0; s->oy[dp] = y0; s->ow[dp] = (uint8_t)(x1 - x0); s->oh[dp] = (uint8_t)(y1 - y0); } } /* Добавить прямоугольник в окно fore-клипа слота (объединение «спрайт + * накладные»): по нему fore-проход возвращает куски тайлов и по нему же * расширяет перебор колонок/рядов. */ static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint8_t w, uint8_t h) { if (!s->cw) { s->cx = x; s->cy = y; s->cw = w; s->ch = h; return; } { int x1 = s->cx + (int)s->cw, y1 = s->cy + (int)s->ch; if (x < s->cx) s->cx = x; if (y < s->cy) s->cy = y; if (x + (int)w > x1) x1 = x + (int)w; if (y + (int)h > y1) y1 = y + (int)h; s->cw = (uint8_t)(x1 - s->cx); s->ch = (uint8_t)(y1 - s->cy); } } /* Стирание прошлого кадра персонажа и его накладных. pop_heal_fast, а не * голый gfx_heal: стираем ровно то, что рисовали, а рисовали в подавляющем * большинстве кадров noclip-ядром; клипающий путь остаётся для straddle и * выхода за поле. * * Тело — static: pop_char_draw зовёт его же «страховкой» (см. ниже), а * трамплин выбирается ОБЪЯВЛЕНИЕМ, и вызов через `__banked`-имя стоил бы * 654 такта внутри собственного банка (memory sdcc_banked_call_rules). */ static void cd_heal(uint8_t who) { pop_cdraw_t *s = &pop_cd[who]; uint8_t p = gfx_get_draw_page() & 1; if (s->valid[p]) pop_heal_fast(s->x[p], s->y[p] + POP_YOFF, s->w[p], s->h[p]); if (s->ovalid[p]) pop_heal_fast(s->ox[p], s->oy[p] + POP_YOFF, s->ow[p], s->oh[p]); s->valid[p] = s->ovalid[p] = 0; } void pop_char_heal(uint8_t who) __banked { cd_heal(who); } /* ---- Пропуск неизменившегося кадра (DRAW-COST; контракт — pop_cdraw.h) -- */ /* Снимок ВХОДОВ отрисовки слота. Картинки (image) в снимке нет: её * однозначно определяет пара (charid, frame), и это ГАРАНТИРУЕТ pop_load_frame * в самой отрисовке — без него кадр приходил из кэша, который мог отстать, и * снимок описывал не то, что нарисовано (BUG-SHADOW-STALE-FRAME, разбор в * pop_char_draw). Тайлы вокруг персонажа (clip_char, fore-проход) в снимок * тоже не входят — их изменение ловит pop_cd_dirty. */ typedef struct { uint8_t frame, x, y, action, sword, charid, room, hurt; int8_t dir, ccol, crow; int dx; /* render_dx: straddle-смещение */ } cd_sig_t; static cd_sig_t cd_sig[POP_CH_N][2]; static void cd_sig_make(uint8_t who, cd_sig_t *g) { const pop_char_t *ch = (who == POP_CH_KID) ? &Kid : &Guard; g->frame = ch->frame; g->x = ch->x; g->y = ch->y; g->action = ch->action; g->sword = ch->sword; g->charid = ch->charid; g->room = ch->room; g->hurt = (uint8_t)((who == POP_CH_KID) ? pop_kid_hurt : pop_guard_hurt); g->dir = ch->direction; g->ccol = ch->curr_col; g->crow = ch->curr_row; g->dx = pop_cd[who].render_dx; } /* Совпадает ли снимок страницы с ТЕКУЩИМ состоянием персонажа. * * Отдельно от cd_sig_make намеренно: тот СТРОИТ структуру, и для проверки * это лишняя работа — тринадцать записей в стековый кадр (а значит через * `-n(ix)`), после которых идёт побайтовый цикл сравнения. А зовут проверку * четыре раза за кадр: pop_char_skip_mask дважды, и в ней по два слота. * Здесь сравниваем поля прямо с источником, и первое же расхождение * заканчивает работу — у ДВИГАЮЩЕГОСЯ персонажа это обычно первое поле. */ static uint8_t cd_sig_same(uint8_t who, uint8_t p) { const pop_char_t *ch = (who == POP_CH_KID) ? &Kid : &Guard; const cd_sig_t *g = &cd_sig[who][p]; if (g->frame != ch->frame) return 0; if (g->x != ch->x) return 0; if (g->y != ch->y) return 0; if (g->dir != ch->direction) return 0; if (g->action != ch->action) return 0; if (g->ccol != ch->curr_col) return 0; if (g->crow != ch->curr_row) return 0; if (g->sword != ch->sword) return 0; if (g->charid != ch->charid) return 0; if (g->room != ch->room) return 0; if (g->hurt != (uint8_t)((who == POP_CH_KID) ? pop_kid_hurt : pop_guard_hurt)) return 0; if (g->dx != pop_cd[who].render_dx) return 0; return 1; } /* Габарит слота на странице: спрайт + накладные (клинок/брызги). y — * КОМНАТНЫЙ (как в pop_cd), перевод в экранный делает вызывающий. */ static void cd_bbox(uint8_t who, uint8_t p, int *x0, int *y0, int *x1, int *y1) { const pop_cdraw_t *s = &pop_cd[who]; *x0 = s->x[p]; *y0 = s->y[p]; *x1 = *x0 + (int)s->w[p]; *y1 = *y0 + (int)s->h[p]; if (s->ovalid[p]) { if (s->ox[p] < *x0) *x0 = s->ox[p]; if (s->oy[p] < *y0) *y0 = s->oy[p]; if (s->ox[p] + (int)s->ow[p] > *x1) *x1 = s->ox[p] + (int)s->ow[p]; if (s->oy[p] + (int)s->oh[p] > *y1) *y1 = s->oy[p] + (int)s->oh[p]; } } /* ЗАБЫТЬ, ЧТО НАРИСОВАНО, на ОБЕИХ страницах. * * Звать после всего, что переписало экран мимо кадрового конвейера: меню, * QuickLoad, переворот картинки. Иначе снимок слота продолжает утверждать, * что нужный кадр персонажа на странице уже есть, и cd_quiet честно * пропускает отрисовку — персонаж исчезает до первого своего движения * (поймано пользователем на выходе из меню; рядом с факелом бага не видно * ровно потому, что анимация фона метит прямоугольник Кида и пропуск * снимается сама собой). */ void pop_cd_forget(void) __banked { uint8_t i; for (i = 0; i < POP_CH_N; i++) { pop_cd[i].valid[0] = pop_cd[i].valid[1] = 0; pop_cd[i].ovalid[0] = pop_cd[i].ovalid[1] = 0; } } /* «Тихий» слот: спрайт этой страницы цел (heal его не трогал), фон в его * прямоугольнике не трогали, и все входы отрисовки те же, что в момент, * когда этот кадр на эту страницу и рисовали. valid[p] здесь ключевой: он * снимается heal'ом, поэтому решение остаётся согласованным, сколько раз его * ни перепроверяй за кадр. */ static uint8_t cd_quiet(uint8_t who, uint8_t p) { /* Соперника на сцене нет и на ЭТОЙ странице от него ничего не осталось — * стирать и рисовать нечего, слот «тихий». Без этого пустой слот каждый * кадр честно проходил heal + вход в pop_char_draw + fore-проход и стоил * ~8 200 тактов из ничего (замер MAME, комната без стража). */ if (who == POP_CH_OPP && Guard.charid == 0 && !pop_cd[who].valid[p] && !pop_cd[who].ovalid[p]) return 1; if (!pop_cd[who].valid[p]) return 0; if (pop_cd_dirty & (1 << p)) { /* фон правили — но задели ли нас? */ /* СПРАЙТ И НАКЛАДНОЙ (клинок, брызги) — ДВУМЯ ОТДЕЛЬНЫМИ проверками, * а не объединённым bbox. Объединение включает пустой угол между * ними, и он ловит касания, которых на самом деле нет: у стоящего * стража спрайт лежит в колонке 8, клинок уходит влево в колонку 7 на * y 59..65, а пламя факела метит колонку 7 на y 33..50. Прямоугольник * «спрайт + клинок» (x 241..284, y 46..84) пересекается с этой меткой * углом — и персонаж перерисовывался целиком за 148 302 такта * (замер 2026-08-19, сцена 11/15). * * Для skip_mask объединение по-прежнему годится: там вопрос «рядом ли * два слота», и загрубление в бОльшую сторону безопасно. */ if (pop_cd_hit_slot(who, p)) return 0; } return cd_sig_same(who, p); } /* Запас вокруг габарита соседа: за кадр персонаж проходит заметно меньше * (бег — ~11 экранных px), а прыжок с падением укладывается сюда же. */ #define CD_MARGIN 32 uint8_t pop_char_skip_mask(void) __banked { uint8_t p = gfx_get_draw_page() & 1; uint8_t q0 = cd_quiet(POP_CH_KID, p); uint8_t q1 = cd_quiet(POP_CH_OPP, p); if (q0 && q1) return 3; /* оба стоят — пересечься нечему */ if (q0 || q1) { /* Один перерисовывается: его heal (и fore-проход по его окну) может * задеть «тихого» соседа и выесть из него кусок. Прямоугольники * рядом — перерисовываем ОБОИХ. */ uint8_t st = (uint8_t)(q0 ? POP_CH_KID : POP_CH_OPP); uint8_t mv = (uint8_t)(st ^ 1); /* valid[st] тоже: «тихий» слот, в котором на этой странице НИЧЕГО не * нарисовано (соперника на сцене нет), выесть невозможно, а bbox у * него мусорный — правило соседства к нему неприменимо. */ if (pop_cd[st].valid[p] && pop_cd[mv].valid[p]) { int ax0, ay0, ax1, ay1, bx0, by0, bx1, by1; cd_bbox(st, p, &ax0, &ay0, &ax1, &ay1); cd_bbox(mv, p, &bx0, &by0, &bx1, &by1); if (ax0 < bx1 + CD_MARGIN && bx0 < ax1 + CD_MARGIN && ay0 < by1 + CD_MARGIN && by0 < ay1 + CD_MARGIN) return 0; } } return (uint8_t)((q0 ? 1 : 0) | (q1 ? 2 : 0)); } void pop_char_set_render_dx(uint8_t who, int dx) __banked { pop_cd[who].render_dx = dx; } /* Брызги урона (draw_hurt_splash, seg006:2003) — ОДИН порт на всех: у Кида * это image 218 своего chtab_2, у соперника image 1 его chtab_5 (у * add_midtable аргумент `obj_id + 1`, а get_image вычитает единицу обратно * — тот же off-by-one, что расписан в шапке pop_pack_guard.py). Подъём: * `((charid == kid) << 2) + 11`, то есть 15 у Кида и 11 у остальных. * * Рисуются МЕЖДУ персонажем и клинком: draw_kid/draw_guard кладут их в * objtable сразу после персонажа и до клинка (seg008:22A2/22C9), а порядок * записей и есть порядок рисования. */ static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages, int fp_x, int obj_y) { const atlas_t *ap; uint8_t aidx; const uint8_t *img; uint8_t w, h; int qx = fp_x, qy = obj_y; int fw = (Char.direction < 0) ? -5 : 5; /* obj_dx_forward(5) */ if (Char.frame == 178) return; /* chomped: брызг нет */ if (who == POP_CH_KID) { if ((KID_SPLASH_IMAGE >> 3) >= kid_npages) return; /* страница не загружена */ ap = &pages[KID_SPLASH_IMAGE >> 3]; aidx = KID_SPLASH_IMAGE & 7; } else { ap = &pages[0]; aidx = 1; } if (Char.frame == 185 || /* мёртв */ (Char.frame >= 106 && Char.frame < 111)) { /* падение+посадка */ qy += 4; } else if (Char.frame == 177) { /* напоролся на пики */ fw = -fw; /* obj_dx_forward(-5) */ } else { qy -= (who == POP_CH_KID) ? 15 : 11; } qx = scr_x(qx + fw); /* calc_screen_x_coord */ img = (const uint8_t *)atlas_image(ap, aidx); gfx_w0_map(ap->page); /* Габарит БАЙТАМИ: спрайты персонажей и накладных не крупнее 64x64 * (memory pop_sprite_size_limits — весь игровой кадр PoP <= 56x63), * а 16-битные w/h заставляли SDCC держать их в стековом кадре и * считать каждое сравнение парой загрузок. */ w = img[0]; h = img[2]; if (w && h) { int top = qy - (int)h + 1; uint8_t flip = (uint8_t)(Char.direction >= 0); int skip = 0, rows = (int)h; /* поле комнаты 0..192 */ uint8_t dp; if (flip) qx -= (int)w; if (top < 0) { skip = -top; rows -= skip; top = 0; } if (top + rows > 192) rows = 192 - top; if (rows <= 0) return; gfx_set_bank(GFX_BANK_SPRITE); pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ if (pop_onscreen_cols(qx, top + POP_YOFF, w, (uint8_t)rows)) gfx_blit_cols_part_noclip(qx, top + POP_YOFF, img, flip, (uint8_t)skip, (uint8_t)rows); else gfx_blit_cols_part(qx, top + POP_YOFF, img, flip, skip, rows); gfx_set_bank(GFX_BANK_NORMAL); dp = gfx_get_draw_page() & 1; cd_overlay_add(s, dp, qx, top, w, (uint8_t)rows); cd_clip_add(s, qx, top, w, (uint8_t)rows); } } void pop_char_draw(uint8_t who) __banked { pop_cdraw_t *s = &pop_cd[who]; const pop_char_t *ch; const kframe *fr; const atlas_t *pages; /* Атлас БРЫЗГ отдельно от кадра: у оригинала они всегда из «своего» * chtab (Кид — chtab_2 image 218, прочие — chtab_5 image 1, * seg006:2019), а у Тени кадр может прийти из другой половины набора. */ const atlas_t *spl; const uint8_t *img; uint8_t npages, page, idx, dp, flip, hurt, lskip; uint8_t w, h, vis_w; int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr; int bcut; /* срез СНИЗУ (clip_char в перевёрнутом виде) */ /* Страховка пропуска (DRAW-COST): главный цикл мог решить, что слот * «тихий», и heal не звать — а тик после этого персонажа всё-таки * сдвинул. Здесь прошлый кадр уже точно надо стереть. Если heal в этом * кадре был, вызов бесплатен: valid/ovalid уже сняты. */ cd_heal(who); s->cw = 0; /* окно fore-клипа: пусто, пока не нарисовали */ s->fpw = 0; /* габарит кадра: нечего перекрывать */ /* Окно Char (loadkid/loadshad) — как в оригинале: clip_char, футпринт и * брызги работают с АКТИВНЫМ персонажем, а к этому моменту в Char лежит * тот, кто тикал последним. Выгружать назад нечего: отрисовка только * читает. */ if (who == POP_CH_KID) { pop_loadkid(); ch = &Kid; fr = &kid_frame; pages = kidp; npages = kid_npages; spl = kidp; hurt = pop_kid_hurt; } else { pop_loadshad(); ch = &Guard; fr = &pop_gframe; pages = gp; npages = g_ok ? GUARD_PAGES : 0; spl = gp; hurt = pop_guard_hurt; if (Guard.charid == 0) return; /* соперника на сцене нет */ /* Набор атласов обязан идти за ТАБЛИЦЕЙ КАДРОВ — условие берём из * того же места, что load_frame (seg006:0532), чтобы они не * разъехались: вне боевых кадров 150..189 Тень ходит по таблице * КИДА, и `image` из неё индексирует спрайты Кида. */ if (Guard.charid == CHARID_1_SHADOW && pop_shadow_kid_n) { /* У Тени СВОЙ запечённый набор (pop_shadow.h): её вид даёт * наложение спрайта на себя со сдвигом, а пакетный блит так не * умеет. Обе половины лежат в нём же, поэтому подмены на kidp * здесь уже не нужно. */ if (pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) { pages = pop_shadow_fgt; npages = pop_shadow_fgt_n; } else { pages = pop_shadow_kid; npages = pop_shadow_kid_n; } spl = pop_shadow_fgt; /* брызги — из половины chtab_5 */ } else if (!pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) { pages = kidp; npages = kid_npages; } } /* load_fram_det_col (seg008:22F5 / 2329) — кадр грузит САМА отрисовка, * сразу после loadkid/loadshad. У нас его не было: kid_frame/pop_gframe * считались кэшем, который наполняет тик (play_seq -> load_frame), а * отрисовка берёт готовое. Кэш лагает — тот, кто пишет Char.frame МИМО * play_seq, кадр не перезагружает, и рисуется прошлая картинка: * * do_init_shad (guards.c) кладёт тени frame = 15 (стойка) прямой * записью, а pop_gframe в этот момент держит кадр 185 УБИТОГО СТРАЖА * с прошлого уровня (после его смерти pop_guard_tick выходит по * `charid == 0` РАНЬШЕ pop_load_fram_det_col, и кэш не трогается * вообще). В кадре входа в комнату тень рисуется картинкой 33 * (34x38, dx 3, dy 4) вместо 12x41 — да ещё из атласа КИДА, потому что * набор выбирается по Guard.frame, а image приходит из кэша. * * Само по себе это моргнуло бы один кадр. Смертельным его делает * пропуск неизменившегося кадра: снимок пишется по Guard.frame, то есть * страница запоминает «нарисован кадр 15», хотя на ней чужой спрайт. * Тень дальше стоит, снимок совпадает — страница НИКОГДА не * перерисовывается, и дабл-буфер вечно мигает двумя разными позами * (BUG-SHADOW-STALE-FRAME, уровень 6 комната 1). * * Лечим корень, а не симптом: после этой строки image ОДНОЗНАЧНО * определяется парой (charid, frame), и снимок cd_sig снова описывает * ровно то, что нарисовано. determine_col из связки НЕ берём: он пишет * Char.curr_col, которую тут же читает clip_char, а физика колонку ведёт * сама (расхождение с оригиналом — ../docs/impl_diff.md). */ pop_load_frame(); if (fr->image == 255) return; /* пустой кадр */ /* load_frame_to_obj (seg008:1728): НИЗ спрайта на obj_y, левый край на * obj_x. +render_dx — straddle-смещение (∓140), когда комната персонажа * не совпадает с отрисованной: спрайт за кромкой. Коллизия НЕ смещается * — она в кадре своей комнаты, смещение чисто рендерное. */ fwd = (int)ch->x + ((ch->direction < 0) ? -fr->dx : fr->dx) + s->render_dx; obj_x = (fwd << 1) - 116; obj_y = (int)ch->y + fr->dy; if ((int8_t)(fr->flags ^ ch->direction) >= 0) obj_x++; /* even/odd pixel (в ЛОГИЧЕСКОЙ X) */ fp_x = obj_x; /* ЛОГИЧЕСКАЯ X — для футпринта */ /* Позицию кадра публикуем СРАЗУ: её читает физика (check_spike_below — * char_x_left активного персонажа), и она обязана обновиться даже в * кадре, который рисовать нечем. Размер (fpw) — ниже, по факту кадра: * он же служит признаком «есть что перекрывать» для fore-прохода. */ s->fpx = fp_x; s->fpy = obj_y; /* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) -> * экран (320 шир = 32px/тайл), ×320/280 = ×8/7. БЕЗ этого персонаж * уезжает влево от тайлов тем сильнее, чем правее. Через scr_x — * таблицей, без __divsint (см. шапку DIV7). */ obj_x = scr_x(obj_x); page = (uint8_t)(fr->image >> 3); idx = (uint8_t)(fr->image & 7); if (page >= npages) return; /* страница набора не загружена */ img = (const uint8_t *)atlas_image(&pages[page], idx); /* ПЕРЕВОРОТ (зелье инверсии): тот же атлас, но страница с вертикально * зеркальными пикселями — смещения лент и размеры кадров у копии те же, * поэтому меняется ровно номер страницы (pop_vflip.h). Если EMM не * дала страницу — рисуем обычной: неперевёрнутый кадр лучше дыры. */ { uint8_t vpg = pages[page].page; if (pop_upside) { uint8_t f = pop_vflip_page(vpg); if (f) vpg = f; } gfx_w0_map(vpg); } /* Габарит БАЙТАМИ: спрайты персонажей и накладных не крупнее 64x64 * (memory pop_sprite_size_limits — весь игровой кадр PoP <= 56x63), * а 16-битные w/h заставляли SDCC держать их в стековом кадре и * считать каждое сравнение парой загрузок. */ w = img[0]; h = img[2]; dp = gfx_get_draw_page() & 1; if (w && h) { /* Спрайты нарисованы ЛИЦОМ ВЛЕВО (как в оригинале); seg008:864 — * флип, когда персонаж смотрит ВПРАВО, и тогда xpos -= image->w * (draw_mid). Флип column-major бесплатен, но зеркалит ВНУТРИ * [x,x+w), поэтому при флипе подаём x−w. */ flip = (uint8_t)(ch->direction >= 0); bx = obj_x; if (flip) bx -= (int)w; top = obj_y - (int)h + 1; /* Поле переворачивается вокруг своей середины: строка t уходит в * (POP_PLAYFIELD_H - 1 - t), значит верх кадра [top, top+h) — * в 192 - top - h, что при top = obj_y-h+1 даёт ровно 191 - obj_y. * Пиксели уже зеркальны (страница выше), поэтому дальше всё * считается как обычно: «пропустить skip строк сверху» у зеркальной * ленты означает «срезать снизу» у оригинала — ровно то, что нужно. */ if (pop_upside) top = (POP_PLAYFIELD_H - 1) - obj_y; vis_w = w; lskip = 0; skip = 0; bcut = 0; /* clip_char (seg006:1231/1749): тайл над головой — стена/пол → всё * выше линии полосы своего ряда НЕ рисуется, иначе персонаж * просвечивает сквозь щели кладки при спуске/висе. */ /* clip_char (seg006:1231/1749): тайл над головой — стена/пол → всё * выше линии полосы своего ряда НЕ рисуется. Граница приходит в * ЛОГИЧЕСКИХ координатах поля (y_clip), и при перевороте её нельзя * сравнивать с уже отражённым top: то, что логически «выше линии», * на экране оказывается НИЖЕ неё. Поэтому в перевороте тот же * срез применяется с другого конца — укорачивает кадр снизу * (bcut), а верх (top) остаётся на месте. Строки ленты при этом * не сдвигаются: зеркальная лента отдаёт их в обратном порядке, * так что «убрать последние bcut строк вывода» = «не рисовать * первые bcut строк оригинала», а это ровно то, что просит * clip_char. */ ct = pop_clip_char_top(fp_x, obj_y, w, h); if (!pop_upside) { if (ct > top) { skip = ct - top; if (skip > (int)h) skip = (int)h; /* обрезан целиком */ top = ct; } } else { int top_log = obj_y - (int)h + 1; /* верх кадра ДО отражения */ if (ct > top_log) { bcut = ct - top_log; if (bcut > (int)h) bcut = (int)h; } } /* Вторая половина clip_char: правый косяк двери уровня. Дверь * рисуется ПОД персонажем (вся в backtable), поэтому спрятать * поднимающегося по лестнице можно только срезав спрайт. */ cr = pop_clip_char_right(); if (cr) { int avail = cr - bx; if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */ else if (avail < (int)w) vis_w = (uint8_t)avail; } /* КЛИП ТЕНИ СЛЕВА (seg008:1699): на уровне зеркала она может * показываться ТОЛЬКО СПРАВА от него — * obj_clip_left = 137 + (mirror_column-4)*32. */ if (ch->charid == CHARID_1_SHADOW && pop_current_level == MIRROR_LEVEL && ch->room == MIRROR_ROOM) { int cl = 137 + (MIRROR_COL - 4) * 32; if (bx < cl) { int d = cl - bx; if (d >= (int)vis_w) vis_w = 0; /* вся тень левее линии */ else lskip = (uint8_t)d; } } /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507: * obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за * него (скелет, падающий в пропасть), обязан обрезаться: ниже поля * идёт полоса HP, и без клипа спрайт лез прямо на неё. */ rows = (int)h - skip - bcut; if (top < 0) { skip += -top; rows += top; top = 0; } if (top + rows > 192) rows = 192 - top; gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ if (rows > 0 && vis_w) { /* +YOFF: центрирование */ /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа * идём общим путём: это редкие кадры (подъём в двери уровня раз * за уровень), цена подготовки там не видна. */ if (lskip) /* тень у зеркала: срез слева */ gfx_blit_cols_part_wx(bx, top + POP_YOFF, img, flip, skip, rows, lskip, (uint8_t)(vis_w - lskip)); else if (vis_w == w && pop_onscreen_cols(bx, top + POP_YOFF, w, (uint8_t)rows)) gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, (uint8_t)skip, (uint8_t)rows); else gfx_blit_cols_part_w(bx, top + POP_YOFF, img, flip, skip, rows, (uint8_t)(vis_w == w ? 0 : vis_w)); } else rows = 0; gfx_set_bank(GFX_BANK_NORMAL); /* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над * срезом (мусор/дыра в кладке на второй странице дабл-буфера). */ s->x[dp] = bx + lskip; s->y[dp] = top; s->w[dp] = (uint8_t)(vis_w - lskip); s->h[dp] = (uint8_t)rows; s->valid[dp] = (uint8_t)(rows != 0); if (rows) cd_clip_add(s, bx, top, vis_w, (uint8_t)rows); s->fpw = w; s->fph = h; /* габарит КАДРА (не обрезанный): */ /* по нему считается футпринт */ } pop_dbg_m15(); /* ЗАМЕР: снимок прямоугольника сделан */ if (hurt) cd_splash(s, who, spl, fp_x, obj_y); /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх * персонажа — со своим прямоугольником heal. */ { int sx, sy; uint16_t sw, sh; if (pop_sword_draw(ch, fr, obj_x, obj_y, &sx, &sy, &sw, &sh)) { cd_overlay_add(s, dp, sx, sy, sw, sh); cd_clip_add(s, sx, sy, sw, sh); } } pop_dbg_m16(); /* ЗАМЕР: splash + клинок сделаны */ gfx_w0_unmap(); /* Что именно нарисовано на ЭТОЙ странице — снимок для пропуска * следующих кадров (DRAW-COST). Только если кадр РЕАЛЬНО рисовали: * при нулевом спрайте (`w && h` ложно — пустая запись атласа) блок выше * не выполняется, и x/y/w/h/valid остаются от ПРОШЛОГО кадра этой * страницы. Снимок же брался всегда — и слот залипал: cd_quiet видел * valid = 1 и совпавшую сигнатуру, то есть считал «на странице * нарисовано ровно то, что нужно», а на ней стоял старый спрайт. Так * тень оставалась на двух страницах дабл-буфера в РАЗНЫХ позах и мигала * между ними (BUG-CHAR-STALE-PAGE). Без снимка слот просто не будет * «тихим», и следующий кадр начнётся с heal — старое сотрётся. */ if (w && h) cd_sig_make(who, &cd_sig[who][dp]); } void pop_char_fore(uint8_t who) __banked { pop_cdraw_t *s = &pop_cd[who]; /* Окно fore-клипа — одно на всех, и ставит его КАЖДЫЙ, кто рисует * персонажа. Пока соперник рисовался строго ДО Кида, к моменту прохода * в окне оказывался Кид и всё сходилось само; как только порядок стал * переменным (кто ниже — тот поверх), при «соперник поверх» окно * оставалось ЕГО, и fore-проход Кида отсекался целиком — Кид рисовался * поверх передних столбов. Поэтому окно ставится здесь, а не держится * «по счастливому порядку вызовов». */ pop_fore_set_clip(s->cx, s->cy + POP_YOFF, (int)s->cw, (int)s->ch); if (s->fpw) pop_fore_over_char(who == POP_CH_KID ? &Kid : &Guard, s->fpx, s->fpy, s->fpw, s->fph); pop_dbg_b6(); /* ЗАМЕР: pop_char_fore целиком */ } /* ---- ОТРАЖЕНИЕ В ЗЕРКАЛЕ (check_mirror, seg003:0798) ---------------- * * * Пока Кид стоит на тайле зеркала, оригинал каждый кадр рисует его * зеркальную копию. ОТДЕЛЬНОЙ функцией, а не третьим слотом pop_cd — и это * не экономия, а структура САМОГО оригинала: отражение идёт сокращённым * путём `load_frame_to_obj(); reset_obj_clip(); add_objtable(4)`, а НЕ через * add_kid_to_objtable. У него нет ни клинка, ни брызг, ни fore-прохода, ни * пропуска неизменившегося кадра — гейтить всё это в общем теле значило бы * добавить ветки в самый горячий путь ради двух секунд за игру. * * Char к моменту вызова уже ОТРАЖЁН вызывающим (pop_check_mirror -> * mirror_image), поэтому здесь никакого «зеркалим» нет — обычная математика * кадра, та же, что в pop_char_draw (см. там load_frame_to_obj, seg008:1728; * шесть строк намеренно продублированы, а не вынесены: трогать горячее тело * ради холодного вызова дороже, чем держать их синхронными по комментарию). */ static int mi_x[2], mi_y[2]; static uint16_t mi_w[2], mi_h[2]; static uint8_t mi_valid[2]; void pop_mirror_heal(void) __banked { uint8_t p = gfx_get_draw_page() & 1; if (!mi_valid[p]) return; pop_heal_fast(mi_x[p], mi_y[p] + POP_YOFF, mi_w[p], mi_h[p]); mi_valid[p] = 0; } void pop_mirror_draw(int clip_top) __banked { uint8_t dp = gfx_get_draw_page() & 1; const kframe *fr = &kid_frame; const uint8_t *img; uint8_t page, idx, flip; uint8_t w, h, vis_w; int fwd, obj_x, obj_y, fp_x, bx, top, skip = 0, rows, cl; uint8_t lskip = 0; if (fr->image == 255) return; page = (uint8_t)(fr->image >> 3); idx = (uint8_t)(fr->image & 7); if (page >= kid_npages) return; fwd = (int)Char.x + ((Char.direction < 0) ? -fr->dx : fr->dx); obj_x = (fwd << 1) - 116; obj_y = (int)Char.y + fr->dy; if ((int8_t)(fr->flags ^ Char.direction) >= 0) obj_x++; fp_x = obj_x; /* ЛОГИЧЕСКАЯ X — её ждёт fore-проход */ obj_x = scr_x(obj_x); img = (const uint8_t *)atlas_image(&kidp[page], idx); gfx_w0_map(kidp[page].page); /* Габарит БАЙТАМИ: спрайты персонажей и накладных не крупнее 64x64 * (memory pop_sprite_size_limits — весь игровой кадр PoP <= 56x63), * а 16-битные w/h заставляли SDCC держать их в стековом кадре и * считать каждое сравнение парой загрузок. */ w = img[0]; h = img[2]; if (!w || !h) { gfx_w0_unmap(); return; } flip = (uint8_t)(Char.direction >= 0); bx = flip ? obj_x - (int)w : obj_x; top = obj_y - (int)h + 1; /* Клип СВЕРХУ — y_clip[curr_row+1] оригинала: отражение не должно * вылезать из проёма зеркала вверх. Значение считает вызывающий * (таблица y_clip живёт в pop_map). */ if (clip_top > top) { skip = clip_top - top; top = clip_top; } rows = (int)h - skip; if (top < 0) { skip += -top; rows += top; top = 0; } if (top + rows > 192) rows = 192 - top; vis_w = w; if (rows <= 0) { gfx_w0_unmap(); return; } /* Клип СЛЕВА — obj_clip_left = (curr_col<<5)+9: отражение видно только * внутри проёма зеркала. Настоящей обрезкой колонок * (gfx_blit_cols_part_wx), а не «нарисовать и вернуть фон»: для * column-major левая обрезка стоит столько же, сколько правая. */ cl = ((int)Char.curr_col << 5) + 9; if (bx < cl) { int d = cl - bx; if (d >= (int)vis_w) { gfx_w0_unmap(); return; } /* целиком за краем */ lskip = (uint8_t)d; } gfx_set_bank(GFX_BANK_SPRITE); pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ if (lskip) gfx_blit_cols_part_wx(bx, top + POP_YOFF, img, flip, skip, rows, lskip, (uint8_t)(vis_w - lskip)); else if (pop_onscreen_cols(bx, top + POP_YOFF, vis_w, (uint16_t)rows)) gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, (uint8_t)skip, (uint8_t)rows); else gfx_blit_cols_part_w(bx, top + POP_YOFF, img, flip, skip, rows, 0); gfx_set_bank(GFX_BANK_NORMAL); gfx_w0_unmap(); mi_x[dp] = bx + lskip; mi_y[dp] = top; mi_w[dp] = (uint16_t)(vis_w - lskip); mi_h[dp] = (uint16_t)rows; mi_valid[dp] = 1; /* FORE-ПРОХОД ПОВЕРХ ОТРАЖЕНИЯ — обязателен, и без него картинка неверна. * В оригинале отражение уходит в objtable (add_objtable(4)), а дальше его * накрывает передний слой тайлов на общих основаниях; у зеркала это * fore_id 77 — ПЕРЕДНЯЯ ЧАСТЬ АРКИ. Именно она прячет то, что вылезло из * проёма: ноги ниже нижней кромки и голову с руками выше верхней (видно * на прыжке). Клип obj_clip_top этого НЕ делает — при curr_row = 0 он * равен y_clip[1] = 3, то есть почти не режет; работу выполняет фон. * * Мы рисуем отражение мимо конвейера персонажей, поэтому проход надо * позвать руками — ровно как это делает pop_char_fore для слотов. */ pop_fore_set_clip(bx + lskip, top + POP_YOFF, (int)(vis_w - lskip), rows); pop_fore_over_char(&Char, fp_x, obj_y, w, h); } /* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- * * Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри * тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража * справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2 * id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы * просто не печатаем — полоса и так на чёрном фоне. * * Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через * pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */ #define HP_Y POP_HP_Y /* полосу делит UI: см. pop_cdraw.h */ #define HP_STEP 7 #define HP_KID_FULL 216 #define HP_KID_EMPTY 217 #define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */ /* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ * страницах дабл-буфера (иначе на второй останется старое значение и его * будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг. * * Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80 * собирает неверно — SUB затирает A, и в s уезжает разность (memory * sdcc_z80_cmp_store_a_bug). */ static uint8_t hp_todo; /* сколько страниц ещё обновить */ static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev; /* Заставить перерисовать полосу на обеих страницах: звать при входе в * комнату (фон перерисован целиком и стёр её) и при старте. */ void pop_hp_invalidate(void) __banked { hp_todo = 2; } void pop_hp_draw(void) __banked { uint8_t i, n; uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0); uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0); { /* Изменилось хоть одно число — обновить обе страницы. */ uint8_t k = hitp_curr, km = hitp_max; uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev || gd != hp_gd_prev || gm != hp_gdmax_prev); hp_kid_prev = k; hp_kidmax_prev = km; hp_gd_prev = gd; hp_gdmax_prev = gm; if (changed) hp_todo = 2; } if (hp_todo == 0) return; hp_todo--; /* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE * (в палитре тоже чёрный): вспышки подменяют нулевой цвет, и полоса HP * не должна мигать вместе с игровым полем (pop_ui.h). */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, POP_COL_OUTSIDE); /* Полосу мы делим со статус-строкой (pop_status.h): пока в ней висит * текст, его зону не трогаем — иначе он мигал бы при каждом изменении * жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R, * так что чистить края по отдельности достаточно. */ if (pop_status_ticks) { bar(0, HP_Y + POP_YOFF, POP_STATUS_L - 1, HP_Y + POP_YOFF + 6); bar(POP_STATUS_R + 1, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6); } else { bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6); } /* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */ n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max); for (i = hitp_curr; i < n; i++) pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF); for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++) pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF); /* Страж: только полные деления, справа налево. Скелет полосы не имеет, * мышь (спецсобытие уровня 8) — тоже: draw_guard_hp, seg000:1159. */ if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && Guard.charid != CHARID_24_MOUSE && gd) { const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0); uint8_t w, h; gfx_w0_map(gp[0].page); w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); if (w && h) { /* Атлас стража тоже column-major — блит колоночный. */ n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd); for (i = 0; i < n; i++) { pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP, HP_Y + POP_YOFF, img, 0, 0, 0); } } gfx_w0_unmap(); } }