/* * pop_guard.c — СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h. * * Здесь только то, что должно оставаться достижимым из банка (guards.c): * поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы. * ОТРИСОВКА — в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл). */ #include #include "pop_guard.h" #include "pop_geom.h" #include "pop_level.h" #include "pop_kid.h" /* Char/play_seq — интерпретатор общий */ #include "pop_map.h" /* hitp_* — HP Кида */ #include "pop_state.h" /* pop_immortal — чит бессмертия */ #include "kid_data.h" pop_char_t Guard; uint8_t guardhp_curr, guardhp_max; int8_t guardhp_delta; uint8_t pop_guard_hurt; 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; /* Сид бросков боёвки. В оригинале генератор один на всё, у нас * последовательности разведены по доменам (кладка / анимация тайлов / бой), * см. pop_geom.h — иначе перерисовка стены сбивала бы броски стража. */ pop_rnd_t pop_fight_seed; /* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его * заполняет логика (W1/W2), а читает отрисовка из резидента W3. */ kframe pop_gframe; uint8_t pop_guard_present(void) { return (uint8_t)(Guard.charid != 0 && Guard.alive < 0 && guardhp_curr != 0); } void pop_guard_reset(void) { Guard.charid = 0; Guard.alive = 0; Guard.direction = 0; guardhp_curr = 0; guardhp_delta = 0; 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): * if (Guard.charid != charid_4_skeleton) { * guardhp_delta = -guardhp_curr; * Guard.alive = 0; * } * Скелета чит не убивает — он и в оригинале бессмертен. */ 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; Guard.alive = 0; } /* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */ uint8_t pop_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; } /* 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; /* Брызги и красная вспышка — только на УРОН: и draw_kid (seg008:1643), * и flash_if_hurt (seg003:785) смотрят именно `hitp_delta < 0`. С * условием «дельта не ноль» их получало и зелье здоровья (+1). */ if (hitp_delta < 0) 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; if (guardhp_delta < 0) pop_guard_hurt = 1; /* -> брызги поверх стража */ guardhp_delta = 0; } } /* check_guard_fallout (seg002:0241): персонаж слота Guard провалился НИЖЕ * комнаты. Уйти в комнату снизу он не может (у стража нет перехода), и * оригинал его просто убирает: guards_tile комнаты становится −1, то есть * стража там больше нет и после возврата. У нас тот же смысл — запомнить * состояние с tile = 30 («стража в комнате больше нет»). * * Ветки тени (падает, только если она в свободном полёте) и скелета (тот * возрождается в комнате 3) появятся вместе с L3-SKEL — сейчас в слоте * бывает только обычный страж. * * Полосу HP перерисовывать отдельно не надо: pop_hp_draw сравнивает числа * и сам увидит, что стража не стало. */ /* Куда и как возрождается упавший СКЕЛЕТ (seg002:252): если под комнатой, * из которой он выпал, лежит комната 3 — он появляется там снова. Убить * его нельзя вовсе, падение лишь переносит. */ #define SKEL_REAPPEAR_ROOM 3 #define SKEL_REAPPEAR_X 133 #define SKEL_REAPPEAR_ROW 1 void pop_guard_fallout(void) { if (Guard.charid == 0 || Guard.y < 211) return; if (Guard.charid == CHARID_4_SKELETON) { uint8_t down = pop_room_link(Guard.room, 3 /* вниз */); if (down == SKEL_REAPPEAR_ROOM) { Guard.room = down; Guard.x = SKEL_REAPPEAR_X; Guard.curr_row = SKEL_REAPPEAR_ROW; Guard.direction = 0; /* dir_0_right */ Guard.alive = -1; pop_guard_leave(); /* запомнить его в новой комнате */ return; } } pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0); Guard.charid = 0; Guard.direction = 0; Guard.alive = 0; guardhp_curr = 0; } /* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у * стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где * add_frame = 70 для кадров 102..106 (seg006:535). */ void pop_guard_load_frame(void) { int16_t idx = (int16_t)Guard.frame; if (idx >= 102 && idx < 107) idx += 70; /* add_frame */ idx -= 149; if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; } pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe); } /* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё * держит стража уходящей комнаты. Мёртвого запоминаем вместе с * последовательностью — тогда при возврате он лежит там же, а не встаёт * живым. */ void pop_guard_leave(void) { if (Guard.charid == 0) return; /* стража не было */ pop_guard_state_save(Guard.room, (uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */ Guard.direction, Guard.x, guard_skill, Guard.curr_seq, (uint8_t)(Guard.alive >= 0)); } static int8_t guard_col_from_x(uint8_t xpos); /* определена ниже */ /* ---- Страж ИДЁТ ЗА КИДОМ в соседнюю комнату ------------------------- * * Порт ветки стража из exit_room (seg002:0340..0382) + follow_guard * (seg002:039E). Своим ходом персонаж слота Guard из комнаты не уходит * (его физика гейтится x ∈ [44,211), см. pop_guard_phys_tick), но когда * комнату меняет КИД, движок может перенести стража следом — именно так в * оригинале страж из комнаты 3 оказывается в комнате 2 за отступающим * Кидом. * * Условия оригинала, все обязательные: * - страж есть, ЖИВ и с ВЫНУТЫМ МЕЧОМ (то есть идёт бой); * - переход ВБОК: вверх и вниз оригинал не пускает никогда (там условия * `curr_row >= 0` и `curr_row < 3` истинны всегда → leave_guard); * - в целевой комнате НЕТ своего ЖИВОГО стража (иначе их стало бы два: * проверка `guards_tile >= 30 || guards_seq_hi != 0` — «пусто или * труп»); * - страж прижат к тому краю, в который ушёл Кид: x < 91 при уходе * влево, x >= 165 при уходе вправо. * * side — куда ушёл КИД (0 = left, 1 = right, 2 = up, 3 = down), то есть * roomleave_result оригинала. Возврат: 1 — страж перенесён в new_room * (вызывающий НЕ должен звать ни pop_guard_leave, ни pop_guard_enter), * 0 — остаётся, всё как раньше. */ uint8_t pop_guard_follow(uint8_t new_room, uint8_t side) { uint8_t tile, color, skill; int8_t dir; if (Guard.charid == 0) return 0; /* dir_56_none */ if (Guard.alive >= 0 || Guard.sword != SWORD_2_DRAWN) return 0; if (side > 1) return 0; /* только вбок */ /* «В целевой комнате уже есть живой страж» = страж по данным есть И это * не запомненный труп (у трупа сохранена последовательность). */ if (pop_level_guard(new_room, &tile, &dir, &color, &skill) && pop_guard_state_seq(new_room) == 0) return 0; if (side == 0) { if (Guard.x >= 91) return 0; } /* Кид ушёл влево */ else { if (Guard.x < 165) return 0; } /* Кид ушёл вправо */ /* follow_guard: сначала «стража нет» в ОБЕИХ комнатах (в оригинале * guards_tile = 0xFF). Без этого он раздвоится: в старой комнате его * поднимут из данных при возврате, а в новой — при следующем входе. */ pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0); pop_guard_state_save(new_room, 30, 0, 0, 0, 0, 0); /* goto_other_room (seg002:0486) для стража: комната + перенос X через * шов (влево +140, вправо −140). Ряд при боковом переходе не меняется; * колонку пересчитываем из новой X, как enter_guard. */ Guard.room = new_room; Guard.x = (uint8_t)(side == 0 ? Guard.x + 140 : Guard.x - 140); Guard.curr_col = guard_col_from_x(Guard.x); return 1; } /* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа — * floor((xpos − 7 − 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */ static int8_t guard_col_from_x(uint8_t xpos) { int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN]; int16_t col = x / TILE_SIZEX; if (x % TILE_SIZEX < 0) --col; return (int8_t)col; } void pop_guard_enter(uint8_t room) { uint8_t tile, color, skill; int8_t dir; pop_guard_reset(); /* load_lev_spr (seg000:1112): tbl_guard_type == −1 — стражей на уровне * НЕТ вообще (14/15), их графика даже не грузится. Без этой проверки * на таком уровне полезли бы стражи из данных комнат. */ if (pop_level_guard_type(pop_current_level) < 0) return; if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return; /* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты * палитры общие на экран, и смена посреди кадра дала бы стража в новой * палитре с полосой HP в старой. */ pop_guard_set_palette(color); /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из * колонки тайла, а не берётся из файла. */ Guard.room = room; Guard.curr_row = (int8_t)(tile / 10); Guard.y = (uint8_t)pop_y_land[Guard.curr_row + 1]; Guard.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX); Guard.curr_col = (int8_t)(tile % 10); Guard.direction = dir; /* seg002:196: на уровне со скелетами слот поднимает СКЕЛЕТА, и он * встаёт сразу активным — с вынутым мечом (seq_63), а не в стойке * покоя (seq_77). Стойку ставит ветка ниже, вместе с seq трупа. */ Guard.charid = (pop_level_guard_type(pop_current_level) == 2) ? CHARID_4_SKELETON : CHARID_2_GUARD; Guard.sword = (Guard.charid == CHARID_4_SKELETON) ? SWORD_2_DRAWN : 0; /* sword_0_sheathed */ Guard.alive = -1; Guard.action = 1; /* actions_1_run_jump */ Guard.fall_x = Guard.fall_y = 0; /* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций; * всё, что вне диапазона, оригинал сводит к 3. */ guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3); /* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень]. * extrastrength (seg002:38) ненулевой только у skill 4 (=1). */ guardhp_max = guardhp_curr = (uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0)); guardhp_delta = 0; /* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у * ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит * ровно там, где лёг (порт ветки guards_seq_hi != 0). */ { uint16_t saved_seq = pop_guard_state_seq(room); /* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620) * пересчитывает её из колонки только при загрузке уровня, а дальше * ею владеет leave_guard. Иначе тело при возврате прыгает в центр * тайла. */ if (saved_seq) Guard.x = pop_guard_state_x(room); /* ...и КОЛОНКУ пересчитываем из X (enter_guard, seg002:180: * `Char.curr_col = get_tile_div_mod_m7(Char.x)`). Брать её из * тайла нельзя: leave_guard сохраняет get_tilepos(0, row), то * есть колонку 0 всегда. У живого стража это незаметно (X там * сам считается из колонки), а у ЗАПОМНЕННОГО ТРУПА X * настоящая, и curr_col оставался нулём при теле, лежащем в * колонке 2. Тайл трупа шёл в обходе раньше тайла Кида, и Кид * рисовался ПОВЕРХ тела вместо того, чтобы пройти за ним * (BUG-DRAWORDER-1). Колонка нужна не только отрисовке — на * неё смотрят коллизия и check_can_guard_see_kid. */ Guard.curr_col = guard_col_from_x(Guard.x); pop_loadshad(); if (saved_seq) Char.curr_seq = saved_seq; else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE); pop_saveshad(); pop_guard_play(); /* до первого кадра */ /* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */ if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) { Guard.alive = 1; guardhp_curr = 0; } } }