diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index 552f10f..d3dc601 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -21,6 +21,7 @@ #include "pop_map.h" #include "pop_geom.h" #include "pop_trob.h" +#include "pop_state.h" /* pop_immortal — чит бессмертия */ #define TILE_MIDX 7 /* середина тайла в координатах персонажа */ #define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */ @@ -342,9 +343,142 @@ static void autocontrol_opponent(void) else autocontrol_guard_active(); } -/* play_guard (seg006): ИИ + общий диспетчер. */ +/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */ +#define ACTION_99_HURT 99 +#define ACTION_1_RUN 1 +#define FRAME_154_POKING 154 +#define SEQ_69_ATTACK_WAS_PARRIED 69 +#define SEQ_74_HIT_BY_SWORD 74 +#define SEQ_85_STABBED_TO_DEATH 85 + +/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */ +static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] = + { 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0}; + +/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту. + * Возврат 1 — HP кончились. Чит бессмертия ловим здесь: анимацию удара + * Кид получает, а HP не теряет. */ +static uint8_t take_hp(uint8_t count) +{ + if (Char.charid == CHARID_0_KID) { + if (pop_immortal) return 0; + if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; } + hitp_delta = (int8_t)-(int8_t)count; + } else { + if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; } + guardhp_delta = (int8_t)-(int8_t)count; + } + return 0; +} + +/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу. + * Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр + * «получил удар». + * УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки + * меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map + * пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал + * идёт ровно нашей веткой. */ +static void hurt_by_sword(void) +{ + if (Char.alive >= 0) return; + /* Чит бессмертия перекрывает и «безоружного закалывают насмерть»: + * без этого Кид гибнет с одного удара мимо стойки, и чит бесполезен + * ровно там, где нужен. Анимацию удара оставляем. */ + if (Char.charid == CHARID_0_KID && pop_immortal) { + pop_char_set_seq(SEQ_74_HIT_BY_SWORD); + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + Char.fall_y = 0; + play_seq(); + return; + } + if (Char.sword != SWORD_2_DRAWN) { + take_hp(100); + pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); + } else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) { + pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */ + } else { + pop_char_set_seq(SEQ_74_HIT_BY_SWORD); + } + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + Char.fall_y = 0; + play_seq(); /* TODO: звук 12/13 */ +} + +/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника. + * Бьют кадры 153/154; если соперник в этот момент парирует — удар отбит. */ +static void check_hurting(void) +{ + int16_t distance; + uint8_t cf, of, min_range; + + if (Char.sword != SWORD_2_DRAWN) return; + if (Char.curr_row != Opp.curr_row) return; + cf = Char.frame; + if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return; + + distance = pop_char_opp_dist(); + of = Opp.frame; + if (distance < 0 || distance >= 29 || + (of != FRAME_161_PARRY && of != FRAME_150_PARRY)) { + /* Соперник НЕ парирует. */ + if (cf == FRAME_154_POKING) { + min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12); + distance = pop_char_opp_dist(); + if (distance >= (int16_t)min_range && distance < 29) + Opp.action = ACTION_99_HURT; + } + } else { + /* Парировано. */ + Opp.frame = FRAME_161_PARRY; + if (Char.charid != CHARID_0_KID) justblocked = 4; + pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED); + play_seq(); + } +} + +/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */ +void pop_check_sword_hurting(void) __banked +{ + uint8_t kf = Kid.frame; + if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */ + pop_loadshad_and_opp(); + check_hurting(); + pop_saveshad_and_opp(); + pop_loadkid_and_opp(); + check_hurting(); + pop_savekid_and_opp(); +} + +/* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если + * ранен страж — Киду в этом кадре урон уже не проходит (в оригинале так же: + * его action сбрасывается ДО проверки). */ +void pop_check_sword_hurt(void) __banked +{ + if (Guard.action == ACTION_99_HURT) { + if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN; + pop_loadshad(); + hurt_by_sword(); + pop_saveshad(); + guard_refrac = (int8_t)REFRACTIMER[guard_skill]; + } else if (Kid.action == ACTION_99_HURT) { + pop_loadkid(); + hurt_by_sword(); + pop_savekid(); + } +} + +/* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»: + * пока страж жив (alive < 0) и HP есть — обычный ход; как только HP дошли до + * нуля, он переводится в «умирает» (alive = 0), и дальше тот же control() + * ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто + * доигрывает последовательность. */ static void play_guard(void) { + if (Char.alive < 0 && guardhp_curr == 0) { + Char.alive = 0; + /* on_guard_killed (seg006:1927) — у нас пока только звук победы, + * которого нет; для уровня 1 других эффектов там нет. */ + } autocontrol_opponent(); pop_control(); } diff --git a/applications/PoP/roomtest/pop_cheat.h b/applications/PoP/roomtest/pop_cheat.h index 7a2b43f..814d2c4 100644 --- a/applications/PoP/roomtest/pop_cheat.h +++ b/applications/PoP/roomtest/pop_cheat.h @@ -17,5 +17,6 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */ #define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */ +#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */ #endif diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index 35b9369..8e66c55 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -140,6 +140,7 @@ static void run_jump(void) } static void safe_step(void); /* fwd */ +static void draw_sword(void); /* fwd: нужен в control_standing */ static void forward_pressed(void) { @@ -233,7 +234,32 @@ static void control_standing(void) if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD && get_item_action()) return; - /* Kid charid_0, без меча: остаётся нижняя часть control_standing. */ + /* Ветку «charid != kid && вниз+вперёд -> draw_sword» не портируем: у нас + * соперник до control_standing не доходит, его перехватывает + * control_guard_inactive в самом диспетчере (порядок как в seg005). */ + + /* seg005:352 — Кид с мечом ВИДИТ активного стража: достаёт клинок сам. + * Без этого игрок стоит с мечом в ножнах, пока его рубят. */ + if (pop_have_sword) { + if (offguard != 0 && control_shift >= CONTROL_RELEASED) { + /* Меч убран намеренно (down в бою) — не доставать, пока не + * отпустят Shift; в оригинале это переход на loc_6213. */ + } else if (can_guard_see_kid >= 2) { + int16_t d = pop_char_opp_dist(); + if (d >= -10 && d < 90) { + holding_sword = 1; + /* (word)d < (word)-6 — беззнаковое: истинно для ВСЕГО, кроме + * −6..−1. То есть «страж вплотную за спиной» -> отступить, + * иначе достать меч. */ + if (d >= -6 && d < 0) { back_pressed(); return; } + draw_sword(); + return; + } + } else { + offguard = 0; + } + } + if (control_shift == CONTROL_HELD) { if (control_backward == CONTROL_HELD) back_pressed(); else if (control_up == CONTROL_HELD) up_pressed(); @@ -333,9 +359,11 @@ static void draw_sword(void) { uint8_t seq_id = SEQ_55_DRAW_SWORD; control_forward = control_shift2 = release_arrows(); - if (Char.charid != CHARID_0_KID && Char.charid != CHARID_1_SHADOW) - seq_id = SEQ_90_EN_GARDE; - /* TODO: у Кида здесь play_sound(19) + offguard = 0 — звука пока нет. */ + if (Char.charid == CHARID_0_KID) { + offguard = 0; /* TODO: play_sound(19) — звука пока нет */ + } else if (Char.charid != CHARID_1_SHADOW) { + seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */ + } Char.sword = SWORD_2_DRAWN; seqtbl_offset_char(seq_id); } @@ -455,8 +483,9 @@ static void swordfight(void) control_down = CONTROL_IGNORE; Char.sword = SWORD_0_SHEATHED; if (Char.charid == CHARID_0_KID) { - /* TODO: offguard/holding_sword — вместе с боёвкой Кида. */ - guard_refrac = 9; + offguard = 1; /* убрал намеренно — сразу не доставать */ + guard_refrac = 9; /* и страж на это время придержан */ + holding_sword = 0; seq_id = SEQ_93_PUT_SWORD_AWAY_FAST; } else if (Char.charid == CHARID_1_SHADOW) { seq_id = SEQ_92_PUT_SWORD_AWAY; @@ -505,6 +534,16 @@ static void control_with_sword(void) void pop_control(void) { uint8_t f = Char.frame; + /* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0). + * Из любой стойки он валится в seq_71_dying; остальное управление в + * этом состоянии не обрабатывается. Через это же место проходит чит K + * (pop_guard_kill ставит Guard.alive = 0). */ + if (Char.alive >= 0) { + if (f == FRAME_15_STAND || f == FRAME_166_STAND_INACTIVE || + f == FRAME_158_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD) + seqtbl_offset_char(SEQ_71_DYING); + return; + } /* seg005 control(): в отскоке (bumped) и в свободном падении управление * НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок * прерывает присед после удара (medland/плита по голове) нажатием ↑, и diff --git a/applications/PoP/roomtest/pop_gdraw.c b/applications/PoP/roomtest/pop_gdraw.c index fe8cc4c..8e60450 100644 --- a/applications/PoP/roomtest/pop_gdraw.c +++ b/applications/PoP/roomtest/pop_gdraw.c @@ -17,6 +17,7 @@ #include "pop_guard.h" #include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */ +#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */ #include "kid_data.h" /* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */ @@ -144,3 +145,79 @@ void pop_guard_draw(void) __banked if (w && h) pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction); } + +/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- * + * Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри + * тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража + * справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2 + * id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы + * просто не печатаем — полоса и так на чёрном фоне. + * + * Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через + * pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */ +#define HP_Y 194 +#define HP_STEP 7 +#define HP_KID_FULL 216 +#define HP_KID_EMPTY 217 +#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */ + +/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ + * страницах дабл-буфера (иначе на второй останется старое значение и его + * будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг. + * + * Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80 + * собирает неверно — SUB затирает A, и в s уезжает разность (memory + * sdcc_z80_cmp_store_a_bug). */ +static uint8_t hp_todo; /* сколько страниц ещё обновить */ +static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev; + +/* Заставить перерисовать полосу на обеих страницах: звать при входе в + * комнату (фон перерисован целиком и стёр её) и при старте. */ +void pop_hp_invalidate(void) __banked { hp_todo = 2; } + +void pop_hp_draw(void) __banked +{ + uint8_t i, n; + uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0); + uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0); + + { /* Изменилось хоть одно число — обновить обе страницы. */ + uint8_t k = hitp_curr, km = hitp_max; + uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev || + gd != hp_gd_prev || gm != hp_gdmax_prev); + hp_kid_prev = k; hp_kidmax_prev = km; + hp_gd_prev = gd; hp_gdmax_prev = gm; + if (changed) hp_todo = 2; + } + if (hp_todo == 0) return; + hp_todo--; + + /* Стереть прошлую полосу: фон под ней — чёрный борт. */ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6); + + /* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */ + n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max); + for (i = hitp_curr; i < n; i++) + pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF); + for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++) + pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF); + + /* Страж: только полные деления, справа налево. Скелет полосы не имеет. */ + if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) { + const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0); + uint16_t w, h; + gfx_w0_map(gp[0].page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + if (w && h) { + /* Атлас стража тоже column-major — блит колоночный. */ + n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd); + for (i = 0; i < n; i++) + gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP, + HP_Y + POP_YOFF, img, 0, 0, 0); + } + gfx_w0_unmap(); + } +} diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index 06cf56c..ac85461 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -10,6 +10,7 @@ #include "pop_geom.h" #include "pop_level.h" #include "pop_kid.h" /* Char/play_seq — интерпретатор общий */ +#include "pop_map.h" /* hitp_* — HP Кида */ #include "kid_data.h" pop_char_t Guard; @@ -19,6 +20,7 @@ int8_t can_guard_see_kid; int8_t is_guard_notice; uint8_t guard_skill; int8_t guard_refrac, justblocked, kid_sword_strike; +int8_t offguard, holding_sword; /* Сид бросков боёвки. В оригинале генератор один на всё, у нас * последовательности разведены по доменам (кладка / анимация тайлов / бой), @@ -44,6 +46,7 @@ void pop_guard_reset(void) can_guard_see_kid = 0; is_guard_notice = 0; guard_refrac = justblocked = kid_sword_strike = 0; + offguard = holding_sword = 0; } /* kill guard cheat (seg000:786): @@ -56,11 +59,35 @@ void pop_guard_kill(void) { if (Guard.charid == CHARID_4_SKELETON) return; if (!pop_guard_present()) return; + /* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит + * pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше + * общий control() ставит ему seq_71_dying. */ guardhp_delta = (int8_t)-guardhp_curr; - guardhp_curr = 0; Guard.alive = 0; } +/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить + * её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */ +void pop_do_delta_hp(void) +{ + int16_t hp; + if (hitp_delta) { + hp = (int16_t)hitp_curr + hitp_delta; + if (hp < 0) hp = 0; + if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max; + hitp_curr = (uint8_t)hp; + pop_kid_hurt = 1; /* -> «брызги» поверх Кида */ + hitp_delta = 0; + } + if (guardhp_delta) { + hp = (int16_t)guardhp_curr + guardhp_delta; + if (hp < 0) hp = 0; + if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max; + guardhp_curr = (uint8_t)hp; + guardhp_delta = 0; + } +} + /* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у * стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где * add_frame = 70 для кадров 102..106 (seg006:535). */ diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index a10445b..7d9f532 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -31,6 +31,7 @@ #define SEQ_61_PARRY_AFTER_STRIKE 61 #define SEQ_62_PARRY 62 #define SEQ_66_STRIKE_AFTER_PARRY 66 +#define SEQ_71_DYING 71 #define SEQ_75_STRIKE 75 /* удар (Кид) */ #define SEQ_77_GUARD_STAND_INACTIVE 77 #define SEQ_80_STAND_FLIPPED 80 @@ -95,6 +96,12 @@ extern int8_t is_guard_notice; extern uint8_t guard_skill; extern int8_t guard_refrac, justblocked, kid_sword_strike; +/* Состояние меча КИДА (seg005/seg006): + * offguard — меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не + * достаёт клинок автоматически, даже видя стража; + * holding_sword — клинок в руке (для индикатора HP стража). */ +extern int8_t offguard, holding_sword; + /* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */ extern pop_rnd_t pop_fight_seed; @@ -102,6 +109,11 @@ extern pop_rnd_t pop_fight_seed; * check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */ void pop_check_can_guard_see_kid(void) __banked; +/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в + * кадре как в play_frame: hurting -> hurt -> do_delta_hp. */ +void pop_check_sword_hurting(void) __banked; +void pop_check_sword_hurt(void) __banked; + /* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2, * читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */ extern kframe pop_gframe; @@ -122,6 +134,12 @@ void pop_guard_enter(uint8_t room); /* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE, * как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c * (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */ +/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл + * ПОСЛЕ персонажей — она ничего не перекрывает. */ +void pop_hp_draw(void) __banked; +/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */ +void pop_hp_invalidate(void) __banked; + void pop_guard_heal(void) __banked; void pop_guard_draw(void) __banked; @@ -132,6 +150,10 @@ void pop_guard_reset(void); * при включённых читах — проверку делает вызывающий. */ void pop_guard_kill(void); +/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать + * раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */ +void pop_do_delta_hp(void); + /* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */ void pop_guard_tick(void) __banked; diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 8fa1d0a..3fe2d56 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -92,6 +92,27 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, return 1; } +/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y). + * Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры + * Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */ +void pop_kid_img_blit(uint8_t image, int x, int y) +{ + uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */ + uint8_t idx = (uint8_t)(image & 7); + const uint8_t *img; + uint16_t w, h; + if (pg >= kid_npages) return; + img = (const uint8_t *)atlas_image(&kidp[pg], idx); + gfx_w0_map(kidp[pg].page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + /* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid — + * ради бесплатного горизонтального флипа), поэтому блит обязан быть + * колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной. */ + if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0); + gfx_w0_unmap(); +} + int pop_kid_load(uint8_t npages) { static const char *const names[KID_MAXPAGES] = { @@ -184,13 +205,16 @@ pop_char_t Char; pop_char_t Opp; void pop_loadkid(void) { Char = Kid; } -void pop_savekid(void) { Kid = Char; kid_frame = cur_frame; } +void pop_savekid(void) { Kid = Char; } /* Выгрузка БЕЗ кадра: 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_saveshad(void) { Guard = Char; } void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; } +void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; } +void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; } +void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; } /* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной. * +13 при встречных направлениях: у стоящих лицом друг к другу «ноль» @@ -282,6 +306,15 @@ static void load_frame(void) cur_frame.dy = (int8_t)f[2]; cur_frame.flags = f[3]; cur_frame.sword = f[4]; + /* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного + * персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/ + * pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки + * check_sword_hurting / check_sword_hurt) записывало Киду кадр, который + * на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа + * Кида и рисовал произвольную позу. Теперь владелец один — тот, для + * кого кадр реально загружен. */ + if (Char.charid == CHARID_0_KID) kid_frame = cur_frame; + else pop_gframe = cur_frame; } static uint8_t char_dx_forward(int8_t dx) diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index c02b03b..9c99f90 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -78,6 +78,9 @@ void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */ void pop_loadshad(void); void pop_saveshad(void); void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */ +void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */ +void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */ +void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */ /* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной, * 999 — разные комнаты (порт char_opp_dist, seg006:2135). */ @@ -114,6 +117,10 @@ void kid_draw(void); * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ void kid_draw_splash(void); +/* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP + * (id 216 полное деление / 217 пустое). */ +void pop_kid_img_blit(uint8_t image, int x, int y); + /* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid — * вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */ void kid_heal(void); diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 130e627..619b129 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -114,6 +114,12 @@ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавше uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */ uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */ uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ +uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */ +/* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки + * (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале. + * Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и + * проверен до боёвки, и переводить его на дельту без нужды рискованно. */ +int8_t hitp_delta; static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ @@ -156,7 +162,7 @@ void pop_map_set(uint8_t *fg) __banked { g_fg = fg; } void pop_map_set_room(uint8_t room) __banked { g_room = room; } -void pop_kid_hp_reset(void) __banked { hitp_curr = 3; pop_kid_dead = 0; } +void pop_kid_hp_reset(void) __banked { hitp_curr = hitp_max = 3; hitp_delta = 0; pop_kid_dead = 0; } /* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */ static uint8_t take_hp(uint8_t count) diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 23501c9..63e980c 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -40,6 +40,8 @@ uint8_t pop_tile_at(int8_t col, int8_t row) __banked; * респавн). */ extern uint8_t pop_kid_dead; extern uint8_t hitp_curr; +extern uint8_t hitp_max; /* стартовое HP (для индикатора) */ +extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */ void pop_kid_hp_reset(void) __banked; /* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object). diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index 3c4793b..daf98b6 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -13,3 +13,7 @@ #include "pop_state.h" uint8_t pop_loose_landed; + +/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара, + * но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */ +uint8_t pop_immortal; diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h index a468ba3..85c74c3 100644 --- a/applications/PoP/roomtest/pop_state.h +++ b/applications/PoP/roomtest/pop_state.h @@ -14,4 +14,8 @@ * разбирает логика loose-полов (pop_map, W1/W2). */ extern uint8_t pop_loose_landed; +/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона + * (боёвка в банке, пики в pop_map), переключает главный цикл. */ +extern uint8_t pop_immortal; + #endif diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 5ad8cbf..9051479 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -209,6 +209,7 @@ static void enter_room(uint8_t room) pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ + pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ * per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * а не по свежему backtable. Фаза P0. */ @@ -246,7 +247,7 @@ int main(void) #ifdef ROOMNAV uint8_t nav_prev = 0; #endif - uint8_t cheat_prev = 0; + uint8_t cheat_prev = 0, immo_prev = 0; if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */ puts("pop_bg_load failed"); @@ -323,11 +324,17 @@ int main(void) * стоит, можно спокойно разбирать позу Kid и окклюзию. */ if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x1E)) frozen = 0; - { /* Читы (seg000: switch под if (cheats_enabled)). K — убить - * стража; по фронту нажатия, чтобы не срабатывало каждый кадр. */ + { /* Читы (seg000: switch под if (cheats_enabled)). Все — по + * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр. + * K — убить стража; + * I — бессмертие Кида (наш отладочный, в оригинале его нет: + * иначе боёвку проверять получается ровно три раза). */ uint8_t k = kbd_raw_down(KBD_CHEAT_KILL); - if (pop_cheats && k && !cheat_prev) pop_guard_kill(); + uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO); + if (pop_cheats && k && !cheat_prev) pop_guard_kill(); + if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal; cheat_prev = k; + immo_prev = i; } if (frozen) { PROF(0); /* чёрный: ждём vsync */ @@ -379,6 +386,11 @@ int main(void) kid_tick(); /* play_seq: следующий кадр */ pop_phys_tick(); /* K3: падение/приземление/стена */ pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */ + /* Боёвка, порядок как в play_frame (seg000:872): сначала пометить + * попадания с обеих сторон, потом применить, потом свести HP. */ + pop_check_sword_hurting(); + pop_check_sword_hurt(); + pop_do_delta_hp(); if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ uint8_t down = pop_room_link(cur_room, LINK_DOWN); pop_fell_out = 0; @@ -495,6 +507,7 @@ int main(void) kid_draw_splash(); pop_kid_hurt = 0; } + pop_hp_draw(); /* полоса HP — только при изменении */ PROF(6); /* fore поверх Kid + борта */ pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), kid_fp_width(), kid_fp_height(),