Files
Sprinter-SDCC/applications/PoP/roomtest/pop_cdraw.c
T
snark13 bbf91d10ee DRAW-CHAR: отрисовка одна на всех Char; разгрузка банка 2 (90.4% -> 72.9%)
DRAW-CHAR.  Отрисовка персонажа сведена к одному набору функций над Char —
как физика после GUARD-PHYS.  В оригинале add_kid_to_objtable (seg008:22F0) и
add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются
окном (loadkid/loadshad), набором спрайтов и типом объекта, а
redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе.

  pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот
  POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из
  любого банка без трамплина.  Проход окклюзии тоже один
  (pop_fore_over_char), pop_fore_over_kid больше нет.

Починилось само (расхождения, которые и были ценой дублирования): у
соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг
«мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА.

Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ.
Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация
заведена отдельной задачей DRAW-COST.

MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h).
Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а
const-таблицы — в странице банка, из другого банка их не прочитать; трамплин
же выбирается объявлением, то есть __banked на листе бьёт и по горячим
вызывающим (654 такта).  W1 замаплено всегда — оттуда обе половины зовут
листья прямым call и читают таблицы напрямую.

MEM-BANK2, шаг 2: дедуп внутри банка.  wall_pattern 808 -> 394 и wall_rnd
786 -> 654: четыре ветки по виду стены отличались только набором кусков и
числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE,
порядок вызовов prandom сохранён дословно.

Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии
в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт
макро-инициализатор, массив определяет один pop_kid.c.

Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556,
куча 4301 Б.  tests-host зелёные (65/39/53/1723/1); в MAME комната 1
совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 —
та же раскладка кладки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:25:53 +03:00

456 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <graphics.h>
#include <gfx.h>
#include <sprite.h>
#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();
}
}