/* * 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_guard.h" #include "pop_kid.h" /* атласы/кадр Кида, pop_sword_draw, окна Char */ #include "pop_cdraw.h" #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 "kid_data.h" #include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */ /* Цвет стража (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 { /* Таблица — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а * gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS * читает только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем * запись в локальный буфер — стек гарантированно в W2. Без копии BIOS * забирал мусор с чужой страницы и страж становился невидимым. */ uint8_t buf[64]; const uint8_t *p; uint8_t i; if (color == 0 || color > POP_GUARD_NPAL) return; p = pop_guard_pal[color - 1]; for (i = 0; i < 64; i++) buf[i] = p[i]; gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); } /* ---- Атласы стража (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 { static const char *const guard_names[GUARD_PAGES] = { "GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl", "GUARD\\g3.atl", "GUARD\\g4.atl" }; static const char *const skel_names[GUARD_PAGES] = { "SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl", "SKEL\\g3.atl", 0 }; const char *const *names; int8_t type = pop_level_guard_type(pop_current_level); uint8_t i; if (type < 0) return 0; /* соперников на уровне нет */ names = (type == 2) ? skel_names : guard_names; for (i = 0; i < GUARD_PAGES; i++) { if (names[i] == 0) break; if (atlas_load(&gp[i], names[i]) != 0) return -1; } g_ok = 1; /* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется * из res10.bin по guards_color — это делает pop_guard_set_palette при * входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ if (type == 2) { uint8_t buf[64], k; for (k = 0; k < 64; k++) buf[k] = pop_skel_pal[k]; /* BIOS: буфер в W2 */ gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); } return 0; } 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; } /* Добавить накладной спрайт (клинок / брызги) в overlay-прямоугольник * страницы. Оба мелкие и в одном кадре встречаются редко — их объединение * дешевле третьего heal-слота. */ static void cd_overlay_add(pop_cdraw_t *s, uint8_t dp, int x, int y, uint16_t w, uint16_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] = (uint16_t)(x1 - x0); s->oh[dp] = (uint16_t)(y1 - y0); } } /* Добавить прямоугольник в окно fore-клипа слота (объединение «спрайт + * накладные»): по нему fore-проход возвращает куски тайлов и по нему же * расширяет перебор колонок/рядов. */ static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint16_t w, uint16_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 = (uint16_t)(x1 - s->cx); s->ch = (uint16_t)(y1 - s->cy); } } /* Стирание прошлого кадра персонажа и его накладных. pop_heal_fast, а не * голый gfx_heal: стираем ровно то, что рисовали, а рисовали в подавляющем * большинстве кадров noclip-ядром; клипающий путь остаётся для straddle и * выхода за поле. */ void pop_char_heal(uint8_t who) __banked { 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_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; uint16_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 = (qx + fw) * 8 / 7; /* calc_screen_x_coord */ img = (const uint8_t *)atlas_image(ap, aidx); gfx_w0_map(ap->page); w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); 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); if (pop_onscreen_cols(qx, top + POP_YOFF, w, (uint16_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, (uint16_t)rows); cd_clip_add(s, qx, top, w, (uint16_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; const uint8_t *img; uint8_t npages, page, idx, dp, flip, hurt; uint16_t w, h, vis_w; int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr; 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; hurt = pop_kid_hurt; } else { pop_loadshad(); ch = &Guard; fr = &pop_gframe; pages = gp; npages = g_ok ? GUARD_PAGES : 0; hurt = pop_guard_hurt; if (Guard.charid == 0) return; /* соперника на сцене нет */ } 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. БЕЗ этого персонаж * уезжает влево от тайлов тем сильнее, чем правее. */ obj_x = obj_x * 8 / 7; 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); gfx_w0_map(pages[page].page); w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); 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; vis_w = w; skip = 0; /* clip_char (seg006:1231/1749): тайл над головой — стена/пол → всё * выше линии полосы своего ряда НЕ рисуется, иначе персонаж * просвечивает сквозь щели кладки при спуске/висе. */ ct = pop_clip_char_top(fp_x, obj_y, w, h); if (ct > top) { skip = ct - top; if (skip > (int)h) skip = (int)h; /* обрезан целиком */ top = ct; } /* Вторая половина 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 = (uint16_t)avail; } /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507: * obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за * него (скелет, падающий в пропасть), обязан обрезаться: ниже поля * идёт полоса HP, и без клипа спрайт лез прямо на неё. */ rows = (int)h - skip; if (top < 0) { skip += -top; rows += top; top = 0; } if (top + rows > 192) rows = 192 - top; gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ if (rows > 0 && vis_w) { /* +YOFF: центрирование */ /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа * идём общим путём: это редкие кадры (подъём в двери уровня раз * за уровень), цена подготовки там не видна. */ if (vis_w == w && pop_onscreen_cols(bx, top + POP_YOFF, 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, (uint8_t)(vis_w == w ? 0 : vis_w)); } else rows = 0; gfx_set_bank(GFX_BANK_NORMAL); /* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над * срезом (мусор/дыра в кладке на второй странице дабл-буфера). */ s->x[dp] = bx; s->y[dp] = top; s->w[dp] = vis_w; s->h[dp] = (uint16_t)rows; s->valid[dp] = (uint8_t)(rows != 0); if (rows) cd_clip_add(s, bx, top, vis_w, (uint16_t)rows); s->fpw = w; s->fph = h; /* габарит КАДРА (не обрезанный): */ /* по нему считается футпринт */ } if (hurt) cd_splash(s, who, pages, 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); } } gfx_w0_unmap(); } 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); } /* ---- Индикаторы 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 194 #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--; /* Стереть прошлую полосу: фон под ней — чёрный борт. */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); 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); /* Страж: только полные деления, справа налево. Скелет полосы не имеет. */ if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) { const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0); uint16_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++) gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP, HP_Y + POP_YOFF, img, 0, 0, 0); } gfx_w0_unmap(); } }