diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index 1d94f8c..99fe69b 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -10,17 +10,199 @@ * пока исполняется банк, резидентной страницы в адресном пространстве * нет. Поэтому фон/окклюзию за стража рисует главный цикл. * - * Сейчас — каркас: тик есть, ИИ и боёвка (порт seg005 control_guard*, - * seg004 fight/collision) добавляются сюда. + * Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000 + * play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой: + * ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а + * дальше работает общий pop_control(). */ #include "pop_guard.h" #include "pop_kid.h" +#include "pop_ctrl.h" +#include "pop_map.h" +#include "pop_geom.h" +#include "pop_trob.h" +#define TILE_MIDX 7 /* середина тайла в координатах персонажа */ +#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */ + +#define TILE_GATE 4 +#define TILE_DOORTOP_FLOOR 7 +#define TILE_LOOSE 11 +#define TILE_DOORTOP 12 +#define TILE_CHOMPER 18 +#define TILE_WALL 20 + +/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и + * «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его + * через границу банка дороже, чем повторить. */ +static uint8_t tile_is_floor(uint8_t t) +{ + switch (t) { + case 0: case 9: case 12: case 20: + case 26: case 27: case 28: case 29: + return 0; + default: + return 1; + } +} + +/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid, + * seg003:761): xpos − 7 делится на ширину тайла с округлением вниз. */ +static int8_t tile_col; /* колонка последнего tile_at_kid */ + +static uint8_t tile_at_kid(int16_t xpos) +{ + /* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos − 7 − 58)/14), + * СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]). + * Оригинал берёт её из tile_div_tbl, у нас — делением с округлением + * вниз, как в pop_map. */ + int16_t x = xpos - 7 - SCREENSPACE_X; + int16_t col = x / TILE_SIZEX; + if (x % TILE_SIZEX < 0) --col; + tile_col = (int8_t)col; + return pop_tile_at(tile_col, Kid.curr_row); +} + +/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду. + * Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт / + * 2 видит и пойдёт. */ +void pop_check_can_guard_see_kid(void) __banked +{ + uint8_t kid_frame = Kid.frame; + int16_t left_pos, right_pos; + uint8_t tile; + + if (!(Guard.charid != 0 && + /* кадры 219..228 — подъём по лестнице: там страж Кида не видит */ + kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) && + Kid.alive < 0 && Guard.alive < 0 && + Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) { + can_guard_see_kid = 0; + return; + } + + can_guard_see_kid = 2; + left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + if (left_pos > right_pos) { + int16_t t = left_pos; left_pos = right_pos; right_pos = t; + } + /* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */ + if (tile_at_kid(left_pos) == TILE_CHOMPER) + left_pos += TILE_SIZEX; + /* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */ + if (tile_at_kid(right_pos) == TILE_GATE) + right_pos -= TILE_SIZEX; + + while (left_pos <= right_pos) { + tile = tile_at_kid(left_pos); + /* Сквозь это не видно вовсе. */ + if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) { + can_guard_see_kid = 0; + return; + } + /* Видно, но страж не пойдёт: провалится / попадёт под чомпер / + * упрётся в неоткрытые ворота / шагнёт в дыру. */ + if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) { + can_guard_see_kid = 1; + } else if (tile == TILE_GATE && + pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) { + can_guard_see_kid = 1; /* ворота подняты не до конца */ + } + left_pos += TILE_SIZEX; + } +} + +/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной. + * +13 при встречных направлениях: у стоящих лицом друг к другу «ноль» + * приходится на ширину тайла между ними. */ +static int16_t char_opp_dist(void) +{ + int16_t distance; + if (Char.room != Opp.room) return 999; + distance = (int16_t)Opp.x - (int16_t)Char.x; + if (Char.direction < 0) distance = -distance; + if (distance >= 0 && Char.direction != Opp.direction) distance += 13; + return distance; +} + +/* ---- Синтетический ввод (seg002:0706..) --------------------------- */ +static void move_0_nothing(void) +{ + control_shift = control_y = control_x = control_shift2 = 0; + control_down = control_up = control_backward = control_forward = 0; +} +static void move_1_forward(void) +{ + control_x = POP_CONTROL_HELD_FORWARD; + control_forward = POP_CONTROL_HELD; +} +static void move_4_down(void) +{ + control_down = POP_CONTROL_HELD; + control_y = POP_CONTROL_HELD_DOWN; +} +static void move_down_forw(void) +{ + control_down = POP_CONTROL_HELD; + move_1_forward(); +} + +/* autocontrol_guard_inactive (seg002:710): страж с убранным мечом. + * Пока Кид за спиной — страж не реагирует; исключение — «шум» Кида + * (is_guard_notice), от которого страж оборачивается. */ +static void autocontrol_guard_inactive(void) +{ + int16_t distance; + if (Kid.alive >= 0) return; + distance = char_opp_dist(); + /* В оригинале условие записано как (word)distance < (word)-8 — + * беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона −8..−1. + * То есть: Кид не «вплотную за спиной». */ + if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) { + if (is_guard_notice) { + is_guard_notice = 0; + if (distance < 0) { + if (distance < -4) move_4_down(); /* обернуться на шум */ + return; + } + } else if (distance < 0) { + return; + } + } + if (can_guard_see_kid) { + /* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */ + move_down_forw(); + } +} + +/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628) для + * стража. Активная ветка (меч обнажён) — следующим шагом. */ +static void autocontrol_opponent(void) +{ + move_0_nothing(); + if (Char.sword < SWORD_2_DRAWN) + autocontrol_guard_inactive(); +} + +/* play_guard (seg006): ИИ + общий диспетчер. */ +static void play_guard(void) +{ + autocontrol_opponent(); + pop_control(); +} + +/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у + * нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора + * последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) — + * отдельным шагом. */ void pop_guard_tick(void) __banked { if (Guard.charid == 0) return; /* стража в комнате нет */ + pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */ + play_guard(); /* Анимация: та же seqtbl, что у Кида, но своя таблица кадров — * выбирается по Char.charid внутри load_frame (seg006:0293). */ - pop_guard_play(); - /* TODO: порт control_guard/fight (seg005/seg004) — состояние в Guard. */ + play_seq(); + pop_saveshad(); } diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index f28b6ba..edc5da4 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -19,6 +19,7 @@ #include #include "pop_kid.h" #include "pop_map.h" +#include "pop_guard.h" /* charid, состояние меча, seq стража */ /* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в * MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2 @@ -74,7 +75,7 @@ * X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный * автомат held/released/ignore (авто-повтор); ctrl1_* — их память между * кадрами (rest/save). */ -static int8_t control_x, control_y, control_shift; +int8_t control_x, control_y, control_shift; /* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт * СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же * control(), что и у игрока. Поэтому шина общая. */ @@ -324,7 +325,43 @@ static void control_jumpup(void) standing_jump(); } -/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */ +/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём + * клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки + * гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз + * уже как выпад. */ +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 — звука пока нет. */ + Char.sword = SWORD_2_DRAWN; + seqtbl_offset_char(seq_id); +} + +/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в + * кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч, + * вниз без «вперёд» — развернуться (stand flipped). */ +static void control_guard_inactive(void) +{ + if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) { + if (control_forward == CONTROL_HELD) { + draw_sword(); + } else { + control_down = CONTROL_IGNORE; + seqtbl_offset_char(SEQ_80_STAND_FLIPPED); + } + } +} + +/* control_with_sword (seg005:964) — боёвка. Порт следующим шагом; пока + * персонаж с обнажённым мечом просто держит стойку. */ +static void control_with_sword(void) +{ +} + +/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */ void pop_control(void) { uint8_t f = Char.frame; @@ -338,7 +375,13 @@ void pop_control(void) release_arrows(); return; } - if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53)) + /* Порядок веток — как в seg005: меч старше charid, charid старше кадров. + * Пока Kid безоружен, обе ветки — про стража. */ + if (Char.sword == SWORD_2_DRAWN) + control_with_sword(); + else if (Char.charid >= CHARID_2_GUARD) + control_guard_inactive(); + else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53)) control_standing(); else if (f == FRAME_48_TURN) control_turning(); diff --git a/applications/PoP/roomtest/pop_ctrl.h b/applications/PoP/roomtest/pop_ctrl.h index c524eb4..58e2d3f 100644 --- a/applications/PoP/roomtest/pop_ctrl.h +++ b/applications/PoP/roomtest/pop_ctrl.h @@ -39,6 +39,14 @@ int8_t pop_ctrl_shift_held(void); #define POP_CONTROL_RELEASED 0 #define POP_CONTROL_IGNORE 1 #define POP_CONTROL_HELD (-1) +/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх / + * +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он + * заполняет как есть, без flip_control_x. */ +extern int8_t control_x, control_y, control_shift; +#define POP_CONTROL_HELD_FORWARD (-1) +#define POP_CONTROL_HELD_BACKWARD 1 +#define POP_CONTROL_HELD_UP (-1) +#define POP_CONTROL_HELD_DOWN 1 extern int8_t control_forward, control_backward, control_up, control_down, control_shift2; diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index ba645e5..c37af4e 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -15,6 +15,8 @@ pop_char_t Guard; uint8_t guardhp_curr, guardhp_max; int8_t guardhp_delta; +int8_t can_guard_see_kid; +int8_t is_guard_notice; /* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его * заполняет логика (W1/W2), а читает отрисовка из резидента W3. */ @@ -32,6 +34,8 @@ void pop_guard_reset(void) Guard.direction = 0; guardhp_curr = 0; guardhp_delta = 0; + can_guard_see_kid = 0; + is_guard_notice = 0; } /* kill guard cheat (seg000:786): diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 57f9b67..a243a80 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -21,8 +21,16 @@ #include "kid_data.h" /* kframe */ #include "pop_char.h" -/* seqids (types.h:1070) — последовательности стража. */ +/* seqids (types.h:1052+) — последовательности стража. */ +#define SEQ_55_DRAW_SWORD 55 #define SEQ_77_GUARD_STAND_INACTIVE 77 +#define SEQ_80_STAND_FLIPPED 80 +#define SEQ_90_EN_GARDE 90 + +/* frameids (types.h:927) и состояния меча (types.h:332). */ +#define FRAME_166_STAND_INACTIVE 166 +#define SWORD_0_SHEATHED 0 +#define SWORD_2_DRAWN 2 /* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */ #define CHARID_0_KID 0 @@ -37,6 +45,21 @@ extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты extern uint8_t guardhp_curr, guardhp_max; extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */ +/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688): + * 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра), + * 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в + * кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */ +extern int8_t can_guard_see_kid; + +/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного + * стража обернуться, даже если Кид у него за спиной. Ставит источник шума, + * снимает сам ИИ. */ +extern int8_t is_guard_notice; + +/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале + * check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */ +void pop_check_can_guard_see_kid(void) __banked; + /* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2, * читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */ extern kframe pop_gframe; diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 680c16d..a2235c8 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -132,6 +132,12 @@ static kframe kid_frame; /* кадр Kid для kid_draw */ * копирование платится один раз на переключение персонажа. */ 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 в этот @@ -139,6 +145,7 @@ void pop_savekid(void) { Kid = Char; kid_frame = cur_frame; } 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) { @@ -327,6 +334,7 @@ void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir) 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(); /* до первого кадра */ } diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index 4714c84..e4b62ad 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -59,11 +59,15 @@ void play_seq(void); /* работает с Char — см. pop_kid_play * Интерпретатор последовательностей общий для Kid и стража: у них общая * seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */ extern pop_char_t Char; +/* Соперник активного персонажа (порт Opp): ставится вместе с Char через + * pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */ +extern pop_char_t Opp; void pop_loadkid(void); void pop_savekid(void); 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) */ /* Прокрутить последовательность персонажа целиком (load + play_seq + * save). Звать вместо голого play_seq. */ diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index ded72ee..130e627 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -202,6 +202,16 @@ static uint8_t get_tile(int8_t col, int8_t row) return g_curr_tile; } +/* Тайл ТЕКУЩЕЙ комнаты по клетке — наружу (порт get_tile(room,col,row) для + * случая room == текущая). Нужен лучу видимости стража + * (check_can_guard_see_kid, seg003:688): тот идёт по ряду между Кидом и + * стражем, а оба по условию в одной (= отрисованной) комнате. Колонки за + * пределами 0..9 get_tile разрешает через швы — здесь не используются. */ +uint8_t pop_tile_at(int8_t col, int8_t row) __banked +{ + return get_tile(col, row); +} + /* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и * идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто * проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index ad8daa5..23501c9 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -30,6 +30,11 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, * Звать в enter_room. */ void pop_map_set_room(uint8_t room) __banked; +/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт + * get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража + * (guards.c): он идёт по ряду между Кидом и стражем. */ +uint8_t pop_tile_at(int8_t col, int8_t row) __banked; + /* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP. * pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/ * респавн). */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index bd13862..6136872 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -191,7 +191,12 @@ static void enter_room(uint8_t room) } } pop_map_set_room(room); - pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ + /* Kid.room — как в оригинале: «комната персонажа». Полная модель + * (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение + * Kid.room == Guard.room нужно уже сейчас: без него луч видимости + * стража никогда не срабатывает. */ + Kid.room = room; + pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ @@ -356,6 +361,7 @@ int main(void) back = dbuf ? (uint8_t)(front ^ 1) : 0; gfx_set_draw_page(back); + pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_ctrl_tick(); /* ввод -> control(): смена seq */ kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ pop_guard_heal(); /* и прошлый кадр стража */