/* * pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame + * seqtbl-интерпретатор. Данные — kid_data.h. */ #include #include #include #include #include #include #include #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): клинок — * ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража. * Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется * sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет. * Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ * calc_screen_x_coord, x — «вперёд» по направлению персонажа. */ static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT; static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT; static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT; 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; } /* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в * кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч * в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем * замапленной — вызывающий делает gfx_w0_unmap сам. */ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, int obj_x, int obj_y, int *rx, int *ry, uint16_t *rw, uint16_t *rh) { uint8_t sf, id, flip; const uint8_t *simg; uint16_t w, h; int bx, top; if (!sword_ok) return 0; if (!((ch->frame >= 229 && ch->frame < 238) || ch->sword != 0 || (ch->charid == CHARID_2_GUARD && ch->alive < 0))) return 0; sf = (uint8_t)(fr->sword & 0x3F); if (sf == 0 || sf >= KID_NSWORD) return 0; id = SWORD_ID[sf]; if (id == 0xFF) return 0; simg = (const uint8_t *)atlas_image(&swordp, id); gfx_w0_map(swordp.page); w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8)); h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8)); if (!w || !h) return 0; flip = (uint8_t)(ch->direction >= 0); bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]); top = obj_y + (int)SWORD_DY[sf] - (int)h + 1; if (flip) bx -= (int)w; gfx_set_bank(GFX_BANK_SPRITE); gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0); gfx_set_bank(GFX_BANK_NORMAL); *rx = bx; *ry = top; *rw = w; *rh = h; return 1; } 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; /* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят * рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё * ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед * ними: для стража Opp = Kid, для Кида Opp = Guard. */ pop_char_t Opp; 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; } void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; } 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.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */ 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]; /* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона). * Раньше он объединялся с прямоугольником персонажа — но объединение сильно * больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил * почти вдвое шире и выше самого Kid, а heal стоит ровно по площади. * Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение * оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования — * зато клинок/брызги честно уходят под передние грани тайлов). */ static int kid_ox[2], kid_oy[2]; static uint16_t kid_ow[2], kid_oh[2]; static uint8_t kid_ovalid[2]; /* Добавить накладной спрайт в overlay-прямоугольник страницы dp. Клинок и * брызги в одном кадре встречаются редко и оба маленькие — их объединение * дешевле третьего слота. */ static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h) { if (!kid_ovalid[dp]) { kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h; kid_ovalid[dp] = 1; return; } { int x0 = kid_ox[dp], y0 = kid_oy[dp]; int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_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; kid_ox[dp] = x0; kid_oy[dp] = y0; kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0); } } /* Окно fore-клипа = объединение «персонаж + накладные». Считается по * прямоугольникам, ничего не рисует. */ static void kid_fore_clip(uint8_t dp) { int x0, y0, x1, y1; if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; } if (!kid_ovalid[dp]) { pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF, (int)kid_lw[dp], (int)kid_lh[dp]); return; } if (!kid_lvalid[dp]) { pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF, (int)kid_ow[dp], (int)kid_oh[dp]); return; } x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp]; y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp]; x1 = kid_lx[dp] + (int)kid_lw[dp]; y1 = kid_ly[dp] + (int)kid_lh[dp]; if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp]; if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp]; pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0); } /* Метрики последнего 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); /* Брызги — накладной спрайт: свой прямоугольник heal (см. * kid_overlay_add), окно fore пересчитываем объединением. */ { uint8_t dp = gfx_get_draw_page(); kid_overlay_add(dp, bx, top, w, h); kid_fore_clip(dp); } } 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: центрирование */ if (kid_ovalid[p]) gfx_heal(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]); kid_lvalid[p] = kid_ovalid[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_overlay_add). */ { int sx, sy; uint16_t sw, sh; if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh)) kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh); } gfx_w0_unmap(); /* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char): * fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми * тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего: * нулевое окно = fore-проход не трогает экран, что и правильно. */ kid_fore_clip(gfx_get_draw_page() & 1); }