/* * pop_gdraw.c — ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c). * * Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ * целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны * остаться в W1/W2 — из банка (guards.c) резидент W3 недостижим ни прямо, * ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит * ровно между «что рисует» (зовёт только главный цикл → резидент) и «что * знает» (зовёт и банк → W1/W2). * * Писучие статики резидента (--dataseg не трогаем) остаются в обычном * _DATA, то есть в W2 — сюда переезжает КОД, не данные. */ #include #include #include #include "pop_guard.h" #include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */ #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; /* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid: * heal обязан стирать спрайт именно той страницы, в которую рисуем). */ static int gx_l[2], gy_l[2]; static uint16_t gw_l[2], gh_l[2]; static uint8_t gvalid[2]; /* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение * почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит * ровно по площади. Объединение считаем только для окна fore-клипа. */ static int sx_l[2], sy_l[2]; static uint16_t sw_l[2], sh_l[2]; static uint8_t svalid[2]; /* Брызги удара — тоже свой прямоугольник, по той же причине: спрайт мелкий и * уезжает вперёд-вверх от стража. Живут они ОДИН кадр, но стереть их обязана * та же страница, в которую рисовали (иначе остаток виден через кадр). */ static int qx_l[2], qy_l[2]; static uint16_t qw_l[2], qh_l[2]; static uint8_t qvalid[2]; /* Ширина спрайта последнего кадра стража — нужна set_char_collision * (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам: * это свойство кадра, а не буфера. Отставание на кадр допустимо — так же * работает kid_fp_width() у Кида. */ static uint16_t g_last_w; uint16_t pop_guard_fp_width(void) __banked { return g_last_w; } /* Набор спрайтов соперника выбирает ТИП УРОВНЯ (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; } /* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же * причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в * большинстве кадров noclip-ядром. Два прямоугольника, а не объединение * (см. комментарий к sx_l выше). */ void pop_guard_heal(void) __banked { uint8_t p = gfx_get_draw_page() & 1; if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]); if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]); if (qvalid[p]) pop_heal_fast(qx_l[p], qy_l[p] + POP_YOFF, qw_l[p], qh_l[p]); gvalid[p] = svalid[p] = qvalid[p] = 0; } void pop_guard_draw(void) __banked { int obj_x, obj_y, top, fwd, fp_x; uint8_t page, idx, p, flip; const uint8_t *img; uint16_t w, h; /* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам * футпринт (pop_fore_over_char) считается по спрайту стража, поэтому * w/h ниже НЕ трогаем. */ int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0; if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return; /* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */ fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx); obj_x = (fwd << 1) - 116; obj_y = (int)Guard.y + pop_gframe.dy; if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++; fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */ obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */ page = (uint8_t)(pop_gframe.image >> 3); idx = (uint8_t)(pop_gframe.image & 7); if (page >= GUARD_PAGES) return; img = (const uint8_t *)atlas_image(&gp[page], idx); gfx_w0_map(gp[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) { int bx = obj_x; /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507: * obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за * него (скелет, падающий в пропасть), обязан обрезаться: ниже поля * идёт полоса HP, и без клипа спрайт лез прямо на неё. */ int skip = 0, rows; top = obj_y - (int)h + 1; flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */ if (flip) bx -= (int)w; rows = (int)h; if (top < 0) { skip = -top; rows -= skip; top = 0; } if (top + rows > 192) rows = 192 - top; if (rows > 0) { p = gfx_get_draw_page() & 1; gfx_set_bank(GFX_BANK_SPRITE); if (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(bx, top + POP_YOFF, img, flip, skip, rows); gfx_set_bank(GFX_BANK_NORMAL); /* heal — ровно по НАРИСОВАННОМУ (обрезанному) прямоугольнику. */ gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = (uint16_t)rows; gvalid[p] = 1; clip_x = bx; clip_y = top; clip_w = w; clip_h = (uint16_t)rows; } g_last_w = w; /* для char_width_half, см. выше */ } /* Брызги удара (draw_hurt_splash, seg006:16CE) — ровно здесь, между * стражем и клинком: draw_guard кладёт их в objtable СРАЗУ после стража * и ДО клинка (seg008:22C9), а порядок записей задаёт порядок рисования. * * Спрайт — chtab_5_guard `obj_id = 1`, то есть image 1 нашего атласа * стража (страница 0, индекс 1): у add_midtable аргумент `obj_id + 1`, а * get_image вычитает единицу обратно (тот же off-by-one, что расписан в * шапке pop_pack_guard.py). У Кида это image 218 своего chtab. * * Смещения от кадра персонажа — по трём веткам оригинала; у стража * подъём на 11 (у Кида 15: `((charid == kid) << 2) + 11`). */ if (pop_guard_hurt && Guard.frame != 178 /* chomped: брызг нет */) { int qx = fp_x, qy = obj_y; const uint8_t *qimg = (const uint8_t *)atlas_image(&gp[0], 1); uint16_t qw, qh; int fw = (Guard.direction < 0) ? -5 : 5; /* obj_dx_forward(5) */ if (Guard.frame == 185 || /* мёртв */ (Guard.frame >= 106 && Guard.frame < 111)) { /* падение+посадка */ qy += 4; } else if (Guard.frame == 177) { /* напоролся на пики */ fw = -fw; /* obj_dx_forward(-5) */ } else { qy -= 11; } qx = (qx + fw) * 8 / 7; /* calc_screen_x_coord */ gfx_w0_map(gp[0].page); qw = (uint16_t)(qimg[0] | ((uint16_t)qimg[1] << 8)); qh = (uint16_t)(qimg[2] | ((uint16_t)qimg[3] << 8)); if (qw && qh) { int qtop = qy - (int)qh + 1; uint8_t qflip = (uint8_t)(Guard.direction >= 0); int qskip = 0, qrows = (int)qh; /* клип полем 192, как у спрайта */ if (qflip) qx -= (int)qw; if (qtop < 0) { qskip = -qtop; qrows -= qskip; qtop = 0; } if (qtop + qrows > 192) qrows = 192 - qtop; if (qrows <= 0) goto splash_done; qh = (uint16_t)qrows; gfx_set_bank(GFX_BANK_SPRITE); if (pop_onscreen_cols(qx, qtop + POP_YOFF, qw, qh)) gfx_blit_cols_part_noclip(qx, qtop + POP_YOFF, qimg, qflip, (uint8_t)qskip, (uint8_t)qrows); else gfx_blit_cols_part(qx, qtop + POP_YOFF, qimg, qflip, qskip, qrows); gfx_set_bank(GFX_BANK_NORMAL); p = gfx_get_draw_page() & 1; qx_l[p] = qx; qy_l[p] = qtop; qw_l[p] = qw; qh_l[p] = qh; qvalid[p] = 1; if (!clip_w) { clip_x = qx; clip_y = qtop; clip_w = qw; clip_h = qh; } else { int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h; if (qx < clip_x) clip_x = qx; if (qtop < clip_y) clip_y = qtop; if (qx + (int)qw > x1) x1 = qx + (int)qw; if (qtop + (int)qh > y1) y1 = qtop + (int)qh; clip_w = (uint16_t)(x1 - clip_x); clip_h = (uint16_t)(y1 - clip_y); } } splash_done: ; } /* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable, * seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник * heal; окно fore-клипа — объединение (без него клинок полез бы поверх * переднего столба). */ { int sx, sy; uint16_t sw, sh; if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y, &sx, &sy, &sw, &sh)) { p = gfx_get_draw_page() & 1; sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh; svalid[p] = 1; if (!clip_w) { clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh; } else { int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h; if (sx < clip_x) clip_x = sx; if (sy < clip_y) clip_y = sy; if (sx + (int)sw > x1) x1 = sx + (int)sw; if (sy + (int)sh > y1) y1 = sy + (int)sh; clip_w = (uint16_t)(x1 - clip_x); clip_h = (uint16_t)(y1 - clip_y); } } } gfx_w0_unmap(); /* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c, * fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */ pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h); /* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись * midtable, а foretable рисуется после всех midtable-записей, поэтому * колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid * (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний * столб. */ if (w && h) pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword, clip_x, clip_y + POP_YOFF, clip_w, clip_h, Guard.action, Guard.curr_col, Guard.curr_row); } /* ---- Индикаторы 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(); } }