Files
Sprinter-SDCC/applications/PoP/roomtest/pop_cdraw.c
T
snark13 3913f1eb7d Отражение в зеркале: fore-проход поверх него (ноги/голова вылезали из арки)
Симптом (скриншоты пользователя, ур. 4): у отражения видны ноги ниже
нижней кромки зеркала, а на прыжке — голова и руки выше верхней.  В
оригинале и то и другое скрыто.

Причина: над отражением не шёл fore-проход.  В SDLPoP отражение уходит в
objtable (add_objtable(4), seg003:0798), и передний слой тайлов накрывает
его на общих основаниях — у зеркала это fore_id 77, ПЕРЕДНЯЯ ЧАСТЬ АРКИ.
Именно она прячет всё, что вылезло из проёма.  Клип obj_clip_top этого не
делает и делать не может: при curr_row = 0 он равен y_clip[1] = 3, то есть
режет только по верху поля.

У нас отражение рисуется мимо конвейера персонажей (сокращённый путь, как
и в оригинале), поэтому проход надо звать руками — ровно как это делает
pop_char_fore для слотов: pop_fore_set_clip по нарисованному
прямоугольнику + pop_fore_over_char(&Char, ...) с ЛОГИЧЕСКОЙ x кадра.

Порядок сходится сам: pop_check_mirror стоит до отрисовки персонажей, так
что получается отражение -> его fore -> Кид -> его fore, и каждый ставит
своё окно клипа.

Банк 4: 9590 -> 9770.  tests-host: все 5 наборов прошли.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:19:14 +03:00

820 lines
49 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 "pop_tile.h" /* pop_heal_off — клип тени/отражения через фон */
#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;
}
/* ---- 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_data.bin: 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,
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 и
* выхода за поле.
*
* Тело — 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) -- */
/* Снимок ВХОДОВ отрисовки слота. Кадр анимации (kid_frame/pop_gframe)
* однозначно определяется полем frame, поэтому его самого в снимке нет;
* тайлы вокруг персонажа (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;
}
/* Габарит слота на странице: спрайт + накладные (клинок/брызги). 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];
}
}
/* «Тихий» слот: спрайт этой страницы цел (heal его не трогал), фон в его
* прямоугольнике не трогали, и все входы отрисовки те же, что в момент,
* когда этот кадр на эту страницу и рисовали. valid[p] здесь ключевой: он
* снимается heal'ом, поэтому решение остаётся согласованным, сколько раз его
* ни перепроверяй за кадр. */
static uint8_t cd_quiet(uint8_t who, uint8_t p)
{
cd_sig_t g;
const uint8_t *a, *b;
uint8_t i;
/* Соперника на сцене нет и на ЭТОЙ странице от него ничего не осталось —
* стирать и рисовать нечего, слот «тихий». Без этого пустой слот каждый
* кадр честно проходил 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)) { /* фон правили — но задели ли нас? */
int x0, y0, x1, y1;
cd_bbox(who, p, &x0, &y0, &x1, &y1);
y0 += POP_YOFF; y1 += POP_YOFF; /* метка фона — в ЭКРАННЫХ */
/* cd_bbox отдаёт x1/y1 ИСКЛЮЧИТЕЛЬНО, pop_cd_hit ждёт включительно. */
if (pop_cd_hit(p, x0, y0, x1 - 1, y1 - 1)) return 0;
}
cd_sig_make(who, &g);
a = (const uint8_t *)&g; b = (const uint8_t *)&cd_sig[who][p];
for (i = 0; i < sizeof(cd_sig_t); i++)
if (*a++ != *b++) return 0;
return 1;
}
/* Запас вокруг габарита соседа: за кадр персонаж проходит заметно меньше
* (бег — ~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;
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 = scr_x(qx + fw); /* 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, lskip;
uint16_t w, h, vis_w;
int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr;
/* Страховка пропуска (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;
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; /* соперника на сцене нет */
/* ТЕНЬ (ур. 4) — это зеркальный Кид: вне боевых кадров 150..189 она
* ходит по таблице КИДА (seg006:0532), а `image` из неё индексирует
* спрайты Кида, а не стража. Набор атласов обязан идти за таблицей —
* условие берём из того же места, что load_frame, чтобы они не
* разъехались. */
if (!pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) {
pages = kidp; npages = kid_npages;
}
}
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);
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;
lskip = 0;
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;
}
/* КЛИП ТЕНИ СЛЕВА (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;
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 (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, (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 + lskip; s->y[dp] = top;
s->w[dp] = (uint16_t)(vis_w - lskip); 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();
/* Что именно нарисовано на ЭТОЙ странице — снимок для пропуска
* следующих кадров (DRAW-COST). Только на полном пути: ранние выходы
* оставляют valid[dp] снятым, и cd_quiet отсеет слот и без снимка. */
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);
}
/* ---- ОТРАЖЕНИЕ В ЗЕРКАЛЕ (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;
uint16_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);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
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);
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 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();
}
}