diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 7e9cea1..1661b5a 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c $(PROF_FLAGS) +EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте @@ -112,4 +112,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py -$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) +$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard_cold.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kdraw.c pop_kboot.c _pop_kid.h _pop_kdraw.h pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) diff --git a/applications/PoP/roomtest/_pop_kdraw.h b/applications/PoP/roomtest/_pop_kdraw.h new file mode 100644 index 0000000..f3988d2 --- /dev/null +++ b/applications/PoP/roomtest/_pop_kdraw.h @@ -0,0 +1,34 @@ +/* + * _pop_kdraw.h — ВНУТРЕННИЙ контракт слоя персонажей: pop_cdraw.c (банк 4) + * ↔ pop_kdraw.c (тот же банк 4). + * + * ВАЖНО: объявления здесь НЕ помечены __banked, то есть вызовы идут прямым + * `call` — это корректно РОВНО ПОКА оба модуля лежат в ОДНОЙ странице: + * + * --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c + * + * Линкер этого НЕ проверяет: если один из них переедет в другой банк, + * сборка пройдёт молча, а на устройстве получится прыжок в чужую страницу. + * Меняешь раскладку банков в Makefile — проверь эту пару. + * + * Публичные (из резидента/других банков) входы того же модуля объявлены с + * __banked в pop_kid.h — там pop_sword_load. + */ +#ifndef _POP_KDRAW_H +#define _POP_KDRAW_H + +#include +#include "pop_char.h" +#include "pop_kid.h" + +/* Клинок в руке персонажа (add_sword_to_objtable). Возврат 1 — нарисован, + * и в (*rx,*ry,*rw,*rh) лежит его прямоугольник для heal. Страницу W0 + * оставляет замапленной: размаппит вызывающий. */ +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); + +/* Спрайт из атласов Кида по его id (полоса HP: деления 216/217). */ +void pop_kid_img_blit(uint8_t image, int x, int y); + +#endif /* _POP_KDRAW_H */ diff --git a/applications/PoP/roomtest/_pop_kid.h b/applications/PoP/roomtest/_pop_kid.h new file mode 100644 index 0000000..c07bbca --- /dev/null +++ b/applications/PoP/roomtest/_pop_kid.h @@ -0,0 +1,23 @@ +/* + * _pop_kid.h — ВНУТРЕННИЙ контракт движка Kid: pop_kid.c (резидент W1, + * интерпретатор seqtbl) ↔ pop_kboot.c (банк 8, загрузка ресурсов). + * + * Загрузка атласов Кида и страницы анимации отрабатывает РАЗ ЗА ИГРУ, и + * держать её в дефицитном W1 незачем; интерпретатор же зовут из четырёх + * банков каждый тик — он остался в резиденте. Общее между ними — только + * дескриптор страницы данных: он объявлен здесь и определён в pop_kid.c + * (данные банковых модулей всё равно линкуются в общий _DATA). + */ +#ifndef _POP_KID_H +#define _POP_KID_H + +#include + +/* Смещение данных в странице анимации; ниже 0x100 — ISR-стаб W0. */ +#define KD_DATA_OFF 0x100u + +extern uint8_t kdat_blk; /* EMM-блок страницы kid_data.bin */ +extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */ +extern uint8_t kdat_ok; /* данные загружены */ + +#endif /* _POP_KID_H */ diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index a4c8af6..96f1022 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -19,7 +19,8 @@ #include #include #include "pop_guard.h" -#include "pop_kid.h" /* атласы/кадр Кида, pop_sword_draw, окна Char */ +#include "pop_kid.h" +#include "_pop_kdraw.h" /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */ #include "pop_cdraw.h" #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index 1af736f..e94dbc8 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -195,147 +195,3 @@ void pop_guard_leave(void) 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) -{ - /* Таблицей (резидентная POP_TILE_DIV: −58 уже внутри значений) — тот же - * приём, что в tile_at_kid/get_tile_div_mod. Здесь он кроет ПОЧТИ всё: - * xpos — байт, так что медленный хвост остаётся только для xpos < 7. */ - if (xpos >= 7) return POP_TILE_DIV[xpos - 7]; - { - 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; - } - } -} diff --git a/applications/PoP/roomtest/pop_guard_cold.c b/applications/PoP/roomtest/pop_guard_cold.c new file mode 100644 index 0000000..7a82446 --- /dev/null +++ b/applications/PoP/roomtest/pop_guard_cold.c @@ -0,0 +1,165 @@ +/* + * pop_guard_cold.c — переходы стража МЕЖДУ КОМНАТАМИ (банк 8): страж идёт + * за Кидом (follow_guard, seg002:039E) и вход стража в комнату + * (enter_guard, seg002:0112 + pos_guards, seg003). + * + * Обе зовутся РАЗ НА КОМНАТУ и обе — из roomtest_cold.c, который лежит в + * том же банке 8: значит в резиденте им делать нечего, а трамплин на + * вызове не появляется (объявления в pop_guard.h без __banked; условие + * корректности — оба модуля в одном банке, см. _pop_kdraw.h про ту же + * связку). Состояние стража (Guard, guard_skill, …) живёт в pop_guard.c + * и видно отсюда как обычные глобалы: данные банков — в общем _DATA. + */ +#include +#include "pop_guard.h" +#include "pop_geom.h" +#include "pop_level.h" +#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */ +#include "pop_map.h" +#include "pop_state.h" +#include "pop_tile.h" /* POP_TILE_DIV — колонка по X без деления */ +#include "kid_data.h" + +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) +{ + /* Таблицей (резидентная POP_TILE_DIV: −58 уже внутри значений) — тот же + * приём, что в tile_at_kid/get_tile_div_mod. Здесь он кроет ПОЧТИ всё: + * xpos — байт, так что медленный хвост остаётся только для xpos < 7. */ + if (xpos >= 7) return POP_TILE_DIV[xpos - 7]; + { + 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; + } + } +} diff --git a/applications/PoP/roomtest/pop_kboot.c b/applications/PoP/roomtest/pop_kboot.c new file mode 100644 index 0000000..b339823 --- /dev/null +++ b/applications/PoP/roomtest/pop_kboot.c @@ -0,0 +1,89 @@ +/* + * pop_kboot.c — ЗАГРУЗКА ресурсов Кида: атласы chtab_2 (kid0..27.atl) и + * страница таблиц анимации (kid_data.bin). БАНК 8. + * + * Отрабатывает раз за игру (и на выходе — освобождение), поэтому места в + * резиденте W1 не занимает: контракт с интерпретатором — _pop_kid.h, там + * же дескриптор страницы данных. + */ +#include +#include +#include +#include +#include +#include "pop_kid.h" +#include "_pop_kid.h" +#include "kid_data.h" + +/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с + * замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на + * _gfx_w0_isr (восстановит окно). */ +extern void _gfx_w0_isr(void); + +#define KID_MAXPAGES 28 + +int pop_kid_load(uint8_t npages) __banked +{ + 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) __banked +{ + uint8_t i; + for (i = 0; i < kid_npages; i++) + atlas_free(&kidp[i]); + kid_npages = 0; +} + +/* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна + * W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM- + * странице, которая маппится в W0 РОВНО на время play_seq (раз за тик). + * Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */ +int pop_kid_data_load(const char *path) __banked +{ + 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) __banked +{ + if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; } +} diff --git a/applications/PoP/roomtest/pop_kdraw.c b/applications/PoP/roomtest/pop_kdraw.c new file mode 100644 index 0000000..bab4f21 --- /dev/null +++ b/applications/PoP/roomtest/pop_kdraw.c @@ -0,0 +1,121 @@ +/* + * pop_kdraw.c — отрисовка ИЗ АТЛАСОВ КИДА, которая нужна только слою + * персонажей: клинок в руке (add_sword_to_objtable, seg006:1798) и блит + * спрайта chtab_2 по его id (полоса HP). БАНК 4, вместе с pop_cdraw.c. + * + * Почему здесь. Оба вызова приходят РОВНО из pop_cdraw.c и больше ниоткуда + * (проверено грепом), а лежали они в pop_kid.c — то есть в резиденте W1, + * съедая ~1.2 КБ дефицитного окна ради кода, который зовёт сосед по банку. + * Раз модуль в том же банке 4 — вызовы прямые, без трамплина: объявления в + * _pop_kdraw.h НЕ помечены __banked (см. там же условие корректности). + * + * Загрузка атласа меча (pop_sword_load) осталась публичной и __banked: её + * зовёт инициализация, один раз за игру. + */ +#include +#include +#include +#include "pop_kid.h" +#include "_pop_kdraw.h" +#include "pop_guard.h" /* CHARID_* — клинок есть и у стража */ +#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ +#include "_pop_draw.h" /* pop_onscreen_cols */ +#include "kid_data.h" + +/* 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) __banked +{ + 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->charid == CHARID_4_SKELETON) && + 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; + /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507): клинок + * обрезается по нему так же, как сам персонаж. Без этого у падающего + * в пропасть скелета меч оставался виден НИЖЕ поля, на полосе HP. */ + { + int skip = 0, rows = (int)h; + if (top < 0) { skip = -top; rows -= skip; top = 0; } + if (top + rows > 192) rows = 192 - top; + if (rows <= 0) return 0; + gfx_set_bank(GFX_BANK_SPRITE); + /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h: + * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */ + if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows)) + gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, + (uint8_t)skip, (uint8_t)rows); + else + gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, skip, rows); + gfx_set_bank(GFX_BANK_NORMAL); + *rx = bx; *ry = top; *rw = w; *rh = (uint16_t)rows; + } + return 1; +} + +/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y). + * Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры + * Кида, а держать вторую копию раскладки в pop_cdraw ни к чему. */ +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 выдал бы марку транспонированной. + * noclip БЕЗ проверки (CLIP-1): единственный вызывающий — полоса HP + * (pop_cdraw.c), у неё координаты фиксированные и заведомо на экране. */ + if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0); + gfx_w0_unmap(); +} diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index c479dc7..80f93d8 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -17,6 +17,7 @@ #include "pop_map.h" /* pop_clip_char_top (clip_char) */ #include "pop_geom.h" #include "kid_data.h" +#include "_pop_kid.h" /* дескриптор страницы данных (её грузит банк 8) */ pop_char_t Kid; @@ -48,130 +49,6 @@ uint8_t kid_npages; * kid_set_seq / pop_char_set_seq. */ static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT; -/* 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->charid == CHARID_4_SKELETON) && - 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; - /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507): клинок - * обрезается по нему так же, как сам персонаж. Без этого у падающего - * в пропасть скелета меч оставался виден НИЖЕ поля, на полосе HP. */ - { - int skip = 0, rows = (int)h; - if (top < 0) { skip = -top; rows -= skip; top = 0; } - if (top + rows > 192) rows = 192 - top; - if (rows <= 0) return 0; - gfx_set_bank(GFX_BANK_SPRITE); - /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h: - * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */ - if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows)) - gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, - (uint8_t)skip, (uint8_t)rows); - else - gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, skip, rows); - gfx_set_bank(GFX_BANK_NORMAL); - *rx = bx; *ry = top; *rw = w; *rh = (uint16_t)rows; - } - return 1; -} - -/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y). - * Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры - * Кида, а держать вторую копию раскладки в pop_cdraw ни к чему. */ -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 выдал бы марку транспонированной. - * noclip БЕЗ проверки (CLIP-1): единственный вызывающий — полоса HP - * (pop_cdraw.c), у неё координаты фиксированные и заведомо на экране. */ - 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] = { - "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- @@ -182,7 +59,6 @@ void pop_kid_free(void) * Страница адресуется как у 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), а потом @@ -214,7 +90,9 @@ static const uint8_t *kd_gframe_ptr(uint8_t i) * страницей данных — то есть внутри play_seq. */ #define SEQ(cs) KD_SEQ(cs) -static uint8_t kdat_blk, kdat_page, kdat_ok; +/* Страница данных анимации: заполняет ЗАГРУЗЧИК (pop_kboot.c, банк 8), + * читает интерпретатор здесь. Не static — контракт в _pop_kid.h. */ +uint8_t kdat_blk, kdat_page, kdat_ok; /* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы. * Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на @@ -273,42 +151,6 @@ int16_t pop_char_opp_dist(void) return distance; } -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) — но НЕ в горячем цикле. */ diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index 00ab116..d08fac9 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -29,35 +29,30 @@ extern uint8_t pop_next_level; /* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph. * 0 — OK, -1 — ошибка. */ -int pop_kid_load(uint8_t npages); +int pop_kid_load(uint8_t npages) __banked; /* Данные анимации (таблица кадров + seqtbl) — отдельная EMM-страница * (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО * первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт). * Страница маппится в W0 внутри play_seq, раз за тик. */ -int pop_kid_data_load(const char *path); +int pop_kid_data_load(const char *path) __banked; /* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF * (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard). * Маппит страницу сама; для горячего цикла есть cur_frame. */ void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5); -void pop_kid_data_free(void); +void pop_kid_data_free(void) __banked; /* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах * подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798). * Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда * кадры рисуются без клинка). */ -int pop_sword_load(void); -void pop_kid_free(void); +int pop_sword_load(void) __banked; +void pop_kid_free(void) __banked; -/* Нарисовать клинок chtab_0 поверх персонажа (порт add_sword_to_objtable, - * seg006:1798) — общая точка для Кида и стража: меч в оригинале ОДИН на - * всех. obj_x/obj_y — та же опорная точка, что у спрайта персонажа. - * Возвращает 1 и прямоугольник клинка (для heal), 0 — клинка нет. - * Страницу 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); +/* Сама отрисовка клинка (pop_sword_draw) и блит спрайта chtab_2 по id + * (pop_kid_img_blit) переехали в pop_kdraw.c — БАНК 4, к единственному + * вызывающему (pop_cdraw.c). Их объявления — в _pop_kdraw.h. */ /* Инициализировать Kid: последовательность seq_id, клетка col/row, * направление dir (-1/0). Ставит x/y по x_bump/y_land. */ @@ -125,10 +120,6 @@ uint8_t kid_cur_flags(void); * точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */ void kid_set_seq(uint8_t seq_id); -/* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP - * (id 216 полное деление / 217 пустое). */ -void pop_kid_img_blit(uint8_t image, int x, int y); - /* Атласы Кида (chtab_2) и его текущий кадр — набор слота POP_CH_KID общей * отрисовки персонажей (pop_cdraw.c). Наружу отданы как данные, а не через * аксессоры: читаются раз в кадр из другого банка, где вызов стоил бы diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h index a454396..3c1181c 100644 --- a/applications/PoP/roomtest/pop_tune.h +++ b/applications/PoP/roomtest/pop_tune.h @@ -114,4 +114,11 @@ * В оригинале это условие `current_level == 15` прямо в proc_get_object. */ #define POP_POTIONS_LEVEL 15 +/* С какого уровня начинается игра (-DFIRST_LEVEL=N при сборке). Живёт + * здесь, а не в roomtest.c: старт уровня уехал в холодную половину цикла + * (roomtest_cold.c, банк 8), и константа нужна обеим. */ +#ifndef FIRST_LEVEL +#define FIRST_LEVEL 1 +#endif + #endif /* POP_TUNE_H */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 2866947..4866b26 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -105,10 +105,6 @@ static void kbd_idle(void) #define ROOM_1 1 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ -#ifndef FIRST_LEVEL -#define FIRST_LEVEL 1 /* с какого уровня начинается игра (-DFIRST_LEVEL=N) */ -#endif - #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ @@ -228,58 +224,9 @@ int main(void) uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0; uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0; - /* Набор фона — по типу СТАРТОВОГО уровня. Атласы грузятся ДО initgraph - * (тот снимает DSS-страницу W0); уровень к этому моменту ещё не прочитан, - * поэтому тип берём напрямую от номера. */ - pop_current_level = FIRST_LEVEL; - if (pop_bg_load(pop_level_type()) != 0) { - puts("pop_bg_load failed"); - return 1; - } - pop_cheats = 1; /* режим разработки: читы включены */ - pop_guard_reset(); - if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */ - printf("kid_data.bin load failed\n"); - return 1; - } - pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */ - pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ - if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ - puts("kid atlas load failed"); - return 1; - } - if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */ - puts("level load failed"); - return 1; - } - pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */ - pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */ - pop_checkpoint = 0; /* init_game (seg003:37) */ - - initgraph(); - gfx_set_visible_page(0); - if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */ - gfx_pal_fload(0, "a:\\kid.pal"); - gfx_pal_sync(); /* палитра 0 -> плоскость 1 */ - /* kid.pal — ЕДИНАЯ игровая палитра, собранная из VDUNGEON, и она только - * что затёрла слоты тайлсета. Возвращаем цвета текущего набора: на - * дворцовых уровнях без этого рисуется дворцовая геометрия подземельными - * красками (сине-серое вместо песочного). */ - pop_bg_pal_apply(); - - /* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя - * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ - * страницы, в которую рисуем. Дальше каждый тик восстанавливаем - * ТОЛЬКО прямоугольник Кида (pop_char_heal), а не весь экран. */ - /* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить - * trob/room_modif (ленивая инициализация из уровня при первом входе). */ - /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см. - * pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места, - * Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */ - pop_start_level(); - gfx_set_draw_page(0); - pop_char_draw(POP_CH_KID); - pop_char_fore(POP_CH_KID); + /* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (roomtest_cold.c): + * отрабатывает ровно один раз за игру, а кода там на полкилобайта. */ + if (pop_boot() != 0) return 1; if (pop_ctrl_init() != 0) { /* raw-канал клавиатуры (забирает ввод у DSS) */ closegraph(); @@ -516,40 +463,9 @@ int main(void) } } if (pop_next_level != pop_current_level) { - /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): - * цикл уровня кончается, когда next_level разошёлся с - * current_level (END_LEVEL после seq_70 в двери уровня либо чит - * Shift+L), и play_level грузит следующий. - * - * HP переносится: hitp_beg_lev = hitp_max ДО загрузки — так - * поднятая на уровне «жизнь» (большая склянка) переезжает - * дальше, а урон — нет (do_startpos дольёт до потолка). */ - hitp_beg_lev = hitp_max; - pop_checkpoint = 0; /* уровень пройден (seg003:413) */ - if (pop_level_load_num(pop_next_level) != 0) { - pop_next_level = pop_current_level; /* файла нет — остаёмся */ - } else { - /* Набор спрайтов соперника у каждого уровня СВОЙ - * (load_lev_spr, seg000:1092): уровень 3 — скелет, у него и - * кадров меньше, чем у стража. Без перезагрузки соперник - * рисуется чужим атласом: часть индексов уходит за его - * пределы, и персонаж просто не виден. */ - pop_guard_free(); - pop_guard_load(); - /* Тайлсет: подземелье <-> дворец (порт tbl_envir_ki, - * seg000:1108). Внутри — no-op, если тип не менялся; - * при смене выгружает старые атласы и правит 32 записи - * палитры. Только на границе уровня, не в кадре. */ - if (pop_bg_load(pop_level_type()) != 0) { - puts("bg set load failed"); - return 1; - } - /* Всё состояние прошлого уровня сносит сам pop_start_level: - * pop_trob_reset обнуляет room_seen (модификаторы комнат - * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают - * из неё же, loose чистит enter_room. */ - pop_start_level(); - } + /* Смена уровня целиком — в банке (pop_level_switch): грузится + * файл, спрайты соперника и тайлсет. Раз на уровень. */ + if (pop_level_switch() != 0) return 1; gfx_set_draw_page(back); } if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ @@ -705,13 +621,6 @@ int main(void) } } - gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */ - pop_ctrl_close(); - closegraph(); - pop_bg_free(); - pop_kid_free(); - pop_guard_free(); - pop_kid_data_free(); - pop_level_free(); + pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */ return 0; } diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 84f37a0..882c812 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -22,6 +22,9 @@ #include "pop_cdraw.h" /* pop_cd — инвалидация слотов после переворота */ #include /* gfx_copy_page */ #include "pop_tune.h" +#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */ +#include /* puts/printf — сообщения об ошибках загрузки */ +#include #define SEQ_STAND 2 #define SEQ_TURN 5 @@ -529,3 +532,121 @@ void pop_flip_screen(void) __banked POP_CD_TOUCH_ALL(); /* фон трогали целиком */ pop_hp_invalidate(); /* полосу HP перерисовать */ } + +/* ---- Запуск, смена уровня, завершение ------------------------------- * + * Три куска главного цикла, которые отрабатывают раз за игру (или раз на + * уровень). В резиденте от них оставались только вызовы — сам код здесь. + */ + +int pop_boot(void) __banked +{ + /* Набор фона — по типу СТАРТОВОГО уровня. Атласы грузятся ДО initgraph + * (тот снимает DSS-страницу W0); уровень к этому моменту ещё не прочитан, + * поэтому тип берём напрямую от номера. */ + pop_current_level = FIRST_LEVEL; + if (pop_bg_load(pop_level_type()) != 0) { + puts("pop_bg_load failed"); + return -1; + } + pop_cheats = 1; /* режим разработки: читы включены */ + pop_guard_reset(); + if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */ + printf("kid_data.bin load failed\n"); + return -1; + } + pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */ + pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ + if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ + puts("kid atlas load failed"); + return -1; + } + if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */ + puts("level load failed"); + return -1; + } + pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */ + pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */ + pop_checkpoint = 0; /* init_game (seg003:37) */ + + initgraph(); + gfx_set_visible_page(0); + if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */ + gfx_pal_fload(0, "a:\\kid.pal"); + gfx_pal_sync(); /* палитра 0 -> плоскость 1 */ + /* kid.pal — ЕДИНАЯ игровая палитра, собранная из VDUNGEON, и она только + * что затёрла слоты тайлсета. Возвращаем цвета текущего набора: на + * дворцовых уровнях без этого рисуется дворцовая геометрия подземельными + * красками (сине-серое вместо песочного). */ + pop_bg_pal_apply(); + + /* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя + * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ + * страницы, в которую рисуем. Дальше каждый тик восстанавливаем + * ТОЛЬКО прямоугольник Кида (pop_char_heal), а не весь экран. */ + /* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить + * trob/room_modif (ленивая инициализация из уровня при первом входе). */ + /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см. + * pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места, + * Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */ + pop_start_level(); + gfx_set_draw_page(0); + pop_char_draw(POP_CH_KID); + pop_char_fore(POP_CH_KID); + return 0; +} + +/* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): цикл + * уровня кончается, когда next_level разошёлся с current_level (END_LEVEL + * после seq_70 в двери уровня либо чит Shift+L), и play_level грузит + * следующий. Возврат -1 — фатально (не удалось сменить набор фона). */ +int pop_level_switch(void) __banked +{ + + /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): + * цикл уровня кончается, когда next_level разошёлся с + * current_level (END_LEVEL после seq_70 в двери уровня либо чит + * Shift+L), и play_level грузит следующий. + * + * HP переносится: hitp_beg_lev = hitp_max ДО загрузки — так + * поднятая на уровне «жизнь» (большая склянка) переезжает + * дальше, а урон — нет (do_startpos дольёт до потолка). */ + hitp_beg_lev = hitp_max; + pop_checkpoint = 0; /* уровень пройден (seg003:413) */ + if (pop_level_load_num(pop_next_level) != 0) { + pop_next_level = pop_current_level; /* файла нет — остаёмся */ + } else { + /* Набор спрайтов соперника у каждого уровня СВОЙ + * (load_lev_spr, seg000:1092): уровень 3 — скелет, у него и + * кадров меньше, чем у стража. Без перезагрузки соперник + * рисуется чужим атласом: часть индексов уходит за его + * пределы, и персонаж просто не виден. */ + pop_guard_free(); + pop_guard_load(); + /* Тайлсет: подземелье <-> дворец (порт tbl_envir_ki, + * seg000:1108). Внутри — no-op, если тип не менялся; + * при смене выгружает старые атласы и правит 32 записи + * палитры. Только на границе уровня, не в кадре. */ + if (pop_bg_load(pop_level_type()) != 0) { + puts("bg set load failed"); + return -1; + } + /* Всё состояние прошлого уровня сносит сам pop_start_level: + * pop_trob_reset обнуляет room_seen (модификаторы комнат + * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают + * из неё же, loose чистит enter_room. */ + pop_start_level(); + } + return 0; +} + +void pop_shutdown(void) __banked +{ + gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */ + pop_ctrl_close(); + closegraph(); + pop_bg_free(); + pop_kid_free(); + pop_guard_free(); + pop_kid_data_free(); + pop_level_free(); +} diff --git a/applications/PoP/roomtest/roomtest_cold.h b/applications/PoP/roomtest/roomtest_cold.h index 1798dd6..5613717 100644 --- a/applications/PoP/roomtest/roomtest_cold.h +++ b/applications/PoP/roomtest/roomtest_cold.h @@ -77,4 +77,19 @@ uint8_t guard_over_kid(void) __banked; * переключение, по флагу pop_upside_dirty. */ void pop_flip_screen(void) __banked; +/* ---- Запуск / смена уровня / завершение ------------------------------ * + * Три куска главного цикла с частотой «раз за игру» или «раз на уровень». + * В резиденте от них остались только вызовы. */ + +/* Загрузить ресурсы, поднять графику и палитру, поставить Кида на старт и + * нарисовать первый экран. 0 — OK, -1 — фатально (сообщение уже выведено). */ +int pop_boot(void) __banked; + +/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет, + * старт. Если файла нет — молча остаться на текущем. -1 — фатально. */ +int pop_level_switch(void) __banked; + +/* Снять idle-хук, закрыть ввод и графику, отдать все ресурсы. */ +void pop_shutdown(void) __banked; + #endif /* ROOMTEST_COLD_H */ diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index a6b968b..64c8ead 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -27,9 +27,14 @@ OBJS_geom := build/eng_pop_geom.rel # Char урон идёт через pop_take_hp, и кому именно он достанется (hitp_* или # guardhp_*) решает charid — то есть ровно та логика, которую тесты обязаны # проверять, а не имитировать. +# eng_pop_kboot — загрузчик таблиц анимации: с 2026-08-12 он живёт отдельным +# модулем (в продукте — банк 8), а сцене он нужен: sc_init зовёт +# pop_kid_data_load, и стабы open/read отдают ему настоящий блоб. OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \ + build/eng_pop_kboot.rel \ build/eng_pop_map.rel build/eng_pop_ctrl.rel \ build/eng_pop_state.rel build/eng_pop_guard.rel \ + build/eng_pop_guard_cold.rel \ build/stubs.rel build/scene.rel build/kid_blob.rel \ build/bank_stub.rel