cf06896dbd
Инфраструктура пункта 2. В оригинале у соперника НЕТ своего диспетчера: autocontrol_* (seg002) выставляет те же глобалы control_*, что и ввод игрока, а дальше исполняется тот же control() (seg005:252). Значит перед портом ИИ надо было привести к этому обе половины: - control_forward/backward/up/down/shift2 перестали быть static в pop_ctrl.c — это общая шина синтетического ввода, объявлена в pop_ctrl.h вместе с POP_CONTROL_*; - control() переименован в pop_control() и работает с Char, а не с Kid; - ввод игрока обёрнут в окно Char (loadkid/user_control/savekid), как в play_frame оригинала. Ловушка по дороге (ввод отвалился целиком, Kid не двигался): макрос seqtbl_offset_char вёл на kid_set_seq, который пишет прямо в Kid, а следом savekid затирал Kid копией Char со старой curr_seq. В оригинале seqtbl_offset_char работает именно с Char — макрос переведён на pop_char_set_seq. kid_set_seq остался для вызовов ВНЕ окна (pop_map). Добавлен pop_savekid_state (Kid = Char без кадра): control() кадр не трогает, а cur_frame в этот момент принадлежит тому, кто последним крутил play_seq. Проверено в MAME: бег и упор в стену работают как прежде. Бюджет (комната 3, 125 кадров): 382 584..383 064 против 380 292..381 282, то есть +2 300 тактов на копии окна. 0.891 кадра, запас 46 936. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
567 lines
28 KiB
C
567 lines
28 KiB
C
/*
|
||
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
|
||
* seqtbl-интерпретатор. Данные — kid_data.h.
|
||
*/
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <sprinter.h>
|
||
#include <sprinter_mem.h>
|
||
#include <graphics.h>
|
||
#include <gfx.h>
|
||
#include <sprite.h>
|
||
#include "pop_kid.h"
|
||
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
|
||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
||
#include "pop_geom.h"
|
||
#include "kid_data.h"
|
||
|
||
pop_char_t Kid;
|
||
|
||
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
|
||
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
||
int8_t knock;
|
||
|
||
|
||
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
|
||
#define KID_MAXPAGES 28
|
||
static atlas_t kidp[KID_MAXPAGES];
|
||
static uint8_t kid_npages;
|
||
|
||
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): отдельный
|
||
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237).
|
||
* Из sword_tbl взяты только строки 35..42 — те, что адресуют эти кадры (бой
|
||
* не портирован); индекс в атласе = sword_frame − 35. Смещения таблицы —
|
||
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */
|
||
#define SWORD_SF_LO 35
|
||
#define SWORD_SF_HI 42
|
||
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 };
|
||
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 };
|
||
static atlas_t swordp;
|
||
static uint8_t sword_ok;
|
||
|
||
int pop_sword_load(void)
|
||
{
|
||
if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
|
||
sword_ok = 1;
|
||
return 0;
|
||
}
|
||
|
||
int pop_kid_load(uint8_t npages)
|
||
{
|
||
static const char *const names[KID_MAXPAGES] = {
|
||
"KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
|
||
"KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
|
||
"KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
|
||
"KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
|
||
"KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
|
||
};
|
||
uint8_t i;
|
||
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
|
||
for (i = 0; i < npages; i++)
|
||
if (atlas_load(&kidp[i], names[i]) != 0)
|
||
return -1;
|
||
kid_npages = npages;
|
||
return 0;
|
||
}
|
||
|
||
void pop_kid_free(void)
|
||
{
|
||
uint8_t i;
|
||
for (i = 0; i < kid_npages; i++)
|
||
atlas_free(&kidp[i]);
|
||
kid_npages = 0;
|
||
}
|
||
|
||
/* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
|
||
* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
|
||
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
|
||
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
|
||
* весь остальной код читает cur_frame — 5-байтовую копию текущего кадра.
|
||
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
|
||
* и коллизия/отрисовка работают с ним, а не с таблицей.
|
||
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
||
extern void _gfx_w0_isr(void);
|
||
|
||
#define KD_DATA_OFF 0x100u
|
||
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
|
||
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
|
||
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
|
||
* берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
|
||
* (`ld c,l` / `inc b`). Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
|
||
* −256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
|
||
* пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
|
||
* Артефакт: .sprinter-cc-roomtest/pop_kid.asm, функция load_frame. */
|
||
static const uint8_t *kd_frame_ptr(uint8_t i)
|
||
{
|
||
uint16_t off = (uint16_t)i;
|
||
off += (uint16_t)(off << 2); /* i*5 = i + i*4, без умножения */
|
||
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
|
||
return (const uint8_t *)off;
|
||
}
|
||
|
||
/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
|
||
* страницу данных, см. pop_extract_kid_data.py). */
|
||
static const uint8_t *kd_gframe_ptr(uint8_t i)
|
||
{
|
||
uint16_t off = (uint16_t)i;
|
||
off += (uint16_t)(off << 2);
|
||
off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
|
||
return (const uint8_t *)off;
|
||
}
|
||
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
|
||
(uint16_t)((cs) - SEQTBL_BASE)))
|
||
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
|
||
* страницей данных — то есть внутри play_seq. */
|
||
#define SEQ(cs) KD_SEQ(cs)
|
||
|
||
static uint8_t kdat_blk, kdat_page, kdat_ok;
|
||
|
||
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
|
||
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
|
||
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
|
||
* тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
|
||
static kframe cur_frame;
|
||
static kframe kid_frame; /* кадр Kid для kid_draw */
|
||
|
||
/* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в
|
||
* оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
|
||
* стоят loadkid/savekid и loadshad/saveshad (seg006:809..825). Копия, а
|
||
* не указатель, — и так же, как в оригинале, и быстрее на Z80: обращения
|
||
* в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
|
||
* копирование платится один раз на переключение персонажа. */
|
||
pop_char_t Char;
|
||
|
||
void pop_loadkid(void) { Char = Kid; }
|
||
void pop_savekid(void) { Kid = Char; kid_frame = cur_frame; }
|
||
/* Выгрузка БЕЗ кадра: control() кадр не трогает, а cur_frame в этот
|
||
* момент принадлежит тому, кто последним крутил play_seq. */
|
||
void pop_savekid_state(void) { Kid = Char; }
|
||
void pop_loadshad(void) { Char = Guard; }
|
||
void pop_saveshad(void) { Guard = Char; pop_gframe = cur_frame; }
|
||
|
||
int pop_kid_data_load(const char *path)
|
||
{
|
||
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
||
uint8_t saved_w3, blk;
|
||
uint16_t stub;
|
||
int fd, n;
|
||
|
||
fd = open(path, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
blk = mem_alloc_pages(1);
|
||
if (!blk) { close(fd); return -1; }
|
||
kdat_blk = blk;
|
||
kdat_page = mem_get_page(blk, 0);
|
||
|
||
saved_w3 = _io_page_w3;
|
||
sprinter_page_w3(kdat_page);
|
||
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
|
||
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
|
||
pg[0x38] = 0xC3;
|
||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
||
pg[0x3A] = (uint8_t)(stub >> 8);
|
||
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
|
||
}
|
||
sprinter_page_w3(saved_w3);
|
||
close(fd);
|
||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
|
||
kdat_ok = 1;
|
||
return 0;
|
||
}
|
||
|
||
void pop_kid_data_free(void)
|
||
{
|
||
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
|
||
}
|
||
|
||
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
|
||
* разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF). Сама маппит страницу,
|
||
* поэтому годится для вызова извне (pop_guard) — но НЕ в горячем цикле. */
|
||
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
|
||
{
|
||
const uint8_t *f;
|
||
uint16_t off = (uint16_t)idx;
|
||
if (!kdat_ok) { out5[0] = 255; return; }
|
||
off += (uint16_t)(off << 2); /* idx*5, без умножения */
|
||
off += (uint16_t)(KD_DATA_OFF + tbl_off);
|
||
gfx_w0_map(kdat_page);
|
||
f = (const uint8_t *)off;
|
||
out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
|
||
out5[3] = f[3]; out5[4] = f[4];
|
||
gfx_w0_unmap();
|
||
}
|
||
|
||
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
|
||
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
|
||
static void load_frame(void)
|
||
{
|
||
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
|
||
* стража frame_tbl_guard с индексом frame + add_frame − 149, где
|
||
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||
const uint8_t *f;
|
||
if (Char.charid == CHARID_2_GUARD) {
|
||
int16_t idx = (int16_t)Char.frame;
|
||
if (idx >= 102 && idx < 107) idx += 70;
|
||
idx -= 149;
|
||
if (idx < 0 || idx >= KID_NGFRAMES) { cur_frame.image = 255; return; }
|
||
f = kd_gframe_ptr((uint8_t)idx);
|
||
} else {
|
||
f = kd_frame_ptr(Char.frame);
|
||
}
|
||
cur_frame.image = f[0];
|
||
cur_frame.dx = (int8_t)f[1];
|
||
cur_frame.dy = (int8_t)f[2];
|
||
cur_frame.flags = f[3];
|
||
cur_frame.sword = f[4];
|
||
}
|
||
|
||
static uint8_t char_dx_forward(int8_t dx)
|
||
{
|
||
if (Char.direction < 0) dx = (int8_t)-dx;
|
||
return (uint8_t)(Char.x + dx);
|
||
}
|
||
|
||
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
|
||
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
|
||
int8_t kid_cur_dx(void) { return cur_frame.dx; }
|
||
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
|
||
|
||
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
|
||
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
|
||
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
|
||
|
||
/* То же для АКТИВНОГО персонажа — внутри окна Char (порт
|
||
* seqtbl_offset_char, seg006). Нужен стражу: его последовательность
|
||
* ставится, когда Char уже загружен из Guard. */
|
||
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }
|
||
|
||
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
|
||
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
|
||
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
|
||
* load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
|
||
void play_seq(void)
|
||
{
|
||
if (!kdat_ok) return;
|
||
gfx_w0_map(kdat_page);
|
||
for (;;) {
|
||
uint8_t cmd = SEQ(Char.curr_seq);
|
||
Char.curr_seq++;
|
||
switch (cmd) {
|
||
case 0xFB: /* DX */
|
||
Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
|
||
Char.curr_seq++;
|
||
break;
|
||
case 0xFA: /* DY */
|
||
Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
|
||
Char.curr_seq++;
|
||
break;
|
||
case 0xFE: /* FLIP */
|
||
Char.direction = (int8_t)~Char.direction;
|
||
break;
|
||
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
|
||
Char.curr_seq += 2;
|
||
break;
|
||
case 0xFF: { /* JMP */
|
||
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
|
||
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
|
||
Char.curr_seq = a;
|
||
break;
|
||
}
|
||
case 0xFD: /* UP */
|
||
Char.curr_row--;
|
||
break;
|
||
case 0xFC: /* DOWN */
|
||
Char.curr_row++;
|
||
break;
|
||
case 0xF9: /* ACTION */
|
||
Char.action = SEQ(Char.curr_seq);
|
||
Char.curr_seq++;
|
||
break;
|
||
case 0xF8: /* SET_FALL */
|
||
Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||
Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||
break;
|
||
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
|
||
knock = 1;
|
||
break;
|
||
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
|
||
knock = -1;
|
||
break;
|
||
case 0xF2: /* SOUND — потребить 1 байт */
|
||
Char.curr_seq++;
|
||
break;
|
||
case 0xF1: /* END_LEVEL */
|
||
break;
|
||
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
|
||
* (порт play_seq -> proc_get_object, seg006) */
|
||
if (SEQ(Char.curr_seq) == 1)
|
||
pop_proc_get_object();
|
||
Char.curr_seq++;
|
||
break;
|
||
case 0xF6: /* DIE nop */
|
||
break;
|
||
default: /* КАДР */
|
||
Char.frame = cmd;
|
||
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
|
||
gfx_w0_unmap();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
|
||
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
|
||
* работает с Char и сам по себе не знает, чей это персонаж. */
|
||
void pop_kid_play(void) { pop_loadkid(); play_seq(); pop_savekid(); }
|
||
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }
|
||
|
||
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
|
||
{
|
||
Kid.curr_col = col;
|
||
Kid.curr_row = row;
|
||
Kid.direction = dir;
|
||
Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||
Kid.y = (uint8_t)pop_y_land[row + 1];
|
||
Kid.fall_x = Kid.fall_y = 0;
|
||
Kid.repeat = 0;
|
||
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
|
||
Kid.curr_seq = kid_seq_off[seq_id];
|
||
pop_kid_play(); /* до первого кадра */
|
||
}
|
||
|
||
void kid_tick(void)
|
||
{
|
||
pop_kid_play();
|
||
}
|
||
|
||
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
|
||
* (seg008.c:1728 load_frame_to_obj). НИЗ спрайта на obj_y, левый край
|
||
* на obj_x. Калибровка XOFF/YOFF — сверить с эталоном.
|
||
*
|
||
* Зеркалирование (K2a): спрайты Kid хранятся ЛИЦОМ ВЛЕВО (как в оригинале).
|
||
* seg008.c:864 — флип, когда персонаж смотрит ВПРАВО (direction==dir_0_right
|
||
* ==0), и тогда xpos -= image->w (draw_mid). Реализуем бесплатным
|
||
* горизонтальным флипом column-major спрайта (gfx_blit_cols flip=1):
|
||
* зеркало происходит ВНУТРИ [x,x+w), поэтому при флипе подаём x=obj_x-w,
|
||
* чтобы «hot-point» кадра остался на месте.
|
||
*
|
||
* Kid рисуется банком GFX_BANK_SPRITE (только видео-ОЗУ, НЕ теневая копия
|
||
* — 0xFF прозрачен), чтобы kid_heal() мог восстановить чистый фон из
|
||
* теневой копии (фон запёкся в неё при pop_room_draw). Прямоугольник
|
||
* последней отрисовки запоминается для heal. */
|
||
#define KID_XOFF 0
|
||
#define KID_YOFF 0
|
||
|
||
/* Прямоугольник последней отрисовки Kid — ПО СТРАНИЦЕ (0/1). У каждой
|
||
* графической страницы своя ОЗУ-копия/видео-ОЗУ, поэтому heal должен
|
||
* стирать пиксели Kid именно той страницы, в которую сейчас рисуем
|
||
* (при дабл-буфере страница чередуется — прошлый Kid на этой странице
|
||
* был 2 логических кадра назад). В однобуфере используется только [0]. */
|
||
static int kid_lx[2], kid_ly[2];
|
||
static uint16_t kid_lw[2], kid_lh[2];
|
||
static uint8_t kid_lvalid[2];
|
||
|
||
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
|
||
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
|
||
static int kid_fp_x, kid_fp_y;
|
||
static uint16_t kid_fp_w, kid_fp_h;
|
||
/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room):
|
||
* ∓140 когда комната Kid ≠ отрисованной; 0 обычно. */
|
||
static int kid_render_dx;
|
||
void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; }
|
||
int kid_fp_obj_x(void) { return kid_fp_x; }
|
||
int kid_fp_obj_y(void) { return kid_fp_y; }
|
||
uint16_t kid_fp_width(void) { return kid_fp_w; }
|
||
uint16_t kid_fp_height(void) { return kid_fp_h; }
|
||
|
||
/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт
|
||
* chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27).
|
||
* Позиция от кадра Kid: obj_y −= 15, obj_x += 5 ВПЕРЁД (obj_dx_forward).
|
||
* Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают
|
||
* иначе — тут только общий случай + «на пиках» (порт по мере надобности).
|
||
* Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */
|
||
#define KID_SPLASH_IMAGE 218
|
||
|
||
void kid_draw_splash(void)
|
||
{
|
||
int obj_x = kid_fp_x, obj_y = kid_fp_y - 15;
|
||
uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7;
|
||
const uint8_t *img;
|
||
uint16_t w, h;
|
||
if (page >= kid_npages) return;
|
||
if (Kid.frame == 177) /* напоролся на пики */
|
||
obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5;
|
||
else
|
||
obj_x += (Kid.direction < 0) ? -5 : 5;
|
||
obj_x = obj_x * 8 / 7; /* лог. X -> экран (calc_screen_x_coord) */
|
||
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) {
|
||
uint8_t flip = (Kid.direction >= 0);
|
||
int bx = obj_x;
|
||
int top = obj_y - (int)h + 1;
|
||
if (flip) bx -= (int)w;
|
||
gfx_set_bank(GFX_BANK_SPRITE);
|
||
gfx_blit_cols(bx, top + POP_YOFF, img, flip);
|
||
gfx_set_bank(GFX_BANK_NORMAL);
|
||
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
|
||
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе
|
||
* брызги останутся на экране после кадра. */
|
||
{
|
||
uint8_t dp = gfx_get_draw_page();
|
||
if (kid_lvalid[dp]) {
|
||
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx;
|
||
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
|
||
int x1 = kid_lx[dp] + (int)kid_lw[dp];
|
||
int y1 = kid_ly[dp] + (int)kid_lh[dp];
|
||
if (bx + (int)w > x1) x1 = bx + (int)w;
|
||
if (top + (int)h > y1) y1 = top + (int)h;
|
||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
||
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
|
||
/* Окно fore-слоя расширяем тем же объединением: брызги
|
||
* тоже должны уходить под передние грани тайлов. */
|
||
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
|
||
}
|
||
}
|
||
}
|
||
gfx_w0_unmap();
|
||
}
|
||
|
||
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
|
||
void kid_heal(void)
|
||
{
|
||
uint8_t p = gfx_get_draw_page();
|
||
if (kid_lvalid[p])
|
||
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
|
||
kid_lvalid[p] = 0;
|
||
}
|
||
|
||
void kid_draw(void)
|
||
{
|
||
kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */
|
||
int fwd, obj_x, obj_y, top;
|
||
uint8_t page, idx;
|
||
const uint8_t *img;
|
||
uint16_t w, h;
|
||
|
||
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
|
||
* кадра / страница не загружена) перекрывать нечего, и fore-проход
|
||
* не должен трогать экран. */
|
||
pop_fore_set_clip(0, 0, 0, 0);
|
||
if (fr.image == 255)
|
||
return; /* пустой кадр */
|
||
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
|
||
* отрисованной — спрайт за кромкой. Коллизия (Kid.x в pop_map) НЕ
|
||
* смещается: она в кадре комнаты Kid; смещение только рендерное. */
|
||
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx;
|
||
obj_x = (fwd << 1) - 116;
|
||
obj_y = (int)Kid.y + fr.dy;
|
||
if ((int8_t)(fr.flags ^ Kid.direction) >= 0)
|
||
obj_x++; /* even/odd pixel (в ЛОГИЧЕСКОЙ координате) */
|
||
kid_fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — для футпринта fore */
|
||
kid_fp_y = obj_y;
|
||
/* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) ->
|
||
* экран (320 шир = 32px/тайл), ×320/280 = ×8/7. БЕЗ этого Kid уезжает
|
||
* влево от тайлов тем сильнее, чем правее (col9 ~36px). Коллизия — в
|
||
* логической системе (не масштабируется). */
|
||
obj_x = obj_x * 8 / 7;
|
||
|
||
page = (uint8_t)(fr.image >> 3);
|
||
idx = (uint8_t)(fr.image & 7);
|
||
if (page >= kid_npages)
|
||
return; /* страница не загружена */
|
||
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) {
|
||
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
|
||
int bx = obj_x + KID_XOFF;
|
||
int skip = 0; /* строк спрайта срезано сверху */
|
||
int ct;
|
||
if (flip)
|
||
bx -= (int)w; /* seg008: xpos -= image->w */
|
||
top = obj_y - (int)h + 1 + KID_YOFF;
|
||
uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */
|
||
/* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше
|
||
* линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает
|
||
* сквозь щели кладки при спуске/висе. */
|
||
ct = pop_clip_char_top(kid_fp_x, obj_y, w, h);
|
||
if (ct > top) {
|
||
skip = ct - top;
|
||
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
|
||
}
|
||
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
||
if (skip < (int)h) /* +YOFF: центрирование */
|
||
gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0);
|
||
gfx_set_bank(GFX_BANK_NORMAL);
|
||
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
|
||
* узко, только по прямоугольнику Кида и только при выходе. */
|
||
pop_clip_sprite(bx, w, top + skip + POP_YOFF, (int)h - skip);
|
||
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
|
||
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
|
||
kid_lx[dp] = bx; kid_ly[dp] = top + skip;
|
||
kid_lw[dp] = w; kid_lh[dp] = (uint16_t)((int)h - skip);
|
||
kid_lvalid[dp] = (skip < (int)h);
|
||
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
|
||
}
|
||
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
|
||
* Kid. Прямоугольник heal расширяем объединением — иначе клинок, выйдя
|
||
* за габарит Kid, оставит след. */
|
||
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) {
|
||
uint8_t sf = (uint8_t)(fr.sword & 0x3F);
|
||
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) {
|
||
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
|
||
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
|
||
uint16_t sw, sh;
|
||
gfx_w0_map(swordp.page);
|
||
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
|
||
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
|
||
if (sw && sh) {
|
||
uint8_t sflip = (Kid.direction >= 0);
|
||
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
|
||
: (int)SWORD_DX[si]);
|
||
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
|
||
uint8_t dp = gfx_get_draw_page() & 1;
|
||
if (sflip) sbx -= (int)sw;
|
||
gfx_set_bank(GFX_BANK_SPRITE);
|
||
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
|
||
gfx_set_bank(GFX_BANK_NORMAL);
|
||
if (!kid_lvalid[dp]) {
|
||
kid_lx[dp] = sbx; kid_ly[dp] = stop;
|
||
kid_lw[dp] = sw; kid_lh[dp] = sh;
|
||
kid_lvalid[dp] = 1;
|
||
} else {
|
||
int x0 = kid_lx[dp], y0 = kid_ly[dp];
|
||
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
|
||
if (sbx < x0) x0 = sbx;
|
||
if (stop < y0) y0 = stop;
|
||
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
|
||
if (stop + (int)sh > y1) y1 = stop + (int)sh;
|
||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
||
kid_lw[dp] = (uint16_t)(x1 - x0);
|
||
kid_lh[dp] = (uint16_t)(y1 - y0);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
gfx_w0_unmap();
|
||
/* Окно fore-слоя = итоговый прямоугольник спрайта (с клинком и
|
||
* обрезкой clip_char): fore-проход вернёт куски тайлов ТОЛЬКО поверх
|
||
* него, а не целыми тайлами — см. pop_bg.c, fclip_on. Ставим и когда
|
||
* рисовать нечего (kid_lvalid=0): нулевое окно = fore-проход не
|
||
* трогает экран, что и правильно — перекрывать нечего. */
|
||
{
|
||
uint8_t dp = gfx_get_draw_page() & 1;
|
||
if (kid_lvalid[dp])
|
||
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
|
||
(int)kid_lw[dp], (int)kid_lh[dp]);
|
||
else
|
||
pop_fore_set_clip(0, 0, 0, 0);
|
||
}
|
||
}
|