Files
Sprinter-SDCC/applications/PoP/roomtest/pop_cdraw.c
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

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

1032 lines
65 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"
#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
{
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
};
/* Джафар (уровень 13, tbl_guard_type == 3) — VIZIER.DAT: кадров столько
* же, сколько у стража, страниц пять. */
static const char *const vizier_names[GUARD_PAGES] = {
"VIZIER\\g0.atl", "VIZIER\\g1.atl", "VIZIER\\g2.atl",
"VIZIER\\g3.atl", "VIZIER\\g4.atl"
};
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 : (type == 3) ? vizier_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). */
pop_guard_pal_apply();
return 0;
}
/* Зеркальный набор соперника — пара к pop_guard_load: те же страницы, но
* файлы *_v.atl (см. pop_vflip.h). Здесь, а не в загрузчике набора,
* потому что gp[] и выбор GUARD/SKEL живут именно тут. */
int pop_guard_vflip_load(void) __banked
{
static const char *const guard_v[GUARD_PAGES] = {
"GUARD\\g0_v.atl", "GUARD\\g1_v.atl", "GUARD\\g2_v.atl",
"GUARD\\g3_v.atl", "GUARD\\g4_v.atl"
};
static const char *const skel_v[GUARD_PAGES] = {
"SKEL\\g0_v.atl", "SKEL\\g1_v.atl", "SKEL\\g2_v.atl",
"SKEL\\g3_v.atl", 0
};
const char *const *names;
int8_t type = pop_level_guard_type(pop_current_level);
uint8_t i;
if (type < 0 || !g_ok) return 0; /* соперников на уровне нет */
names = (type == 2) ? skel_v : guard_v;
for (i = 0; i < GUARD_PAGES; i++) {
if (names[i] == 0) break;
if (pop_vflip_add(&gp[i], names[i]) != 0) return -1;
}
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,
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];
}
}
/* «Тихий» слот: спрайт этой страницы цел (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();
}
}