/* * roomtest_cold.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК * (--bank 8=roomtest_cold.c, окно W3). Здесь всё, что отрабатывает раз на * уровень или только по отладочной клавише; кадровый путь остался в * roomtest.c. Зачем разделили и что осталось снаружи — см. roomtest_cold.h. * * Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого * модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие * __banked-модули через трамплины. */ #include #include #include "roomtest_cold.h" #include "pop_bg.h" /* pop_loose_mob_reset — банк 7, зовём через трамплин */ #include "pop_kid.h" #include "pop_map.h" #include "pop_level.h" #include "pop_trob.h" #include "pop_guard.h" #include "pop_state.h" #include "pop_geom.h" #include "pop_cdraw.h" /* pop_cd — инвалидация слотов после переворота */ #include /* gfx_copy_page */ #include "pop_tune.h" #define SEQ_STAND 2 #define SEQ_TURN 5 #define SEQ_FALL 7 #define SEQ_RUN 84 #define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */ #define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */ #define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */ #define TILE_EMPTY 0 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ /* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ — * это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью * (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72). * Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки: * его стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина * той самой двери. */ static void find_start_level_door(uint8_t room) { uint8_t *m = pop_trob_modif(room); uint8_t tp; if (!m) return; /* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз * за уровень, а pop_add_trob сам лезет в W0. */ for (tp = 0; tp < 30; tp++) { if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue; m[tp] = 43; /* створка поднята полностью */ pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */ } } /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos * (seg003:028A). * * Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define: * start_pos — тайл 0..29 (row*10+col), start_dir — направление, в котором * персонаж должен ОКАЗАТЬСЯ. * * Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж * ставится спиной и доворачивается кадрами позы входа. Сама поза — из * tbl_entry_pose (data.h:848 = {0,1,0,…,2,…}): * 1 (уровень 1) — падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2); * это оно захлопывает решётку за спиной в первой комнате; * 2 (уровень 13) — вбегание (seq_84_run); * иначе — разворот на месте (seq_5_turn). * Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */ void pop_start_level(void) __banked { uint8_t entry_pose, start_room, pos, seq = SEQ_TURN, chkp; int8_t dir; pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */ /* upside_down тоже сбрасывается стартом уровня (seg003:38/188). Если * картинка была перевёрнута, отрисовка комнаты ниже уже пойдёт обычной * стороной, поэтому переворачивать ничего не надо — только снять флаги. */ pop_upside = 0; pop_upside_dirty = 0; entry_pose = pop_level_entry_pose(pop_current_level); start_room = pop_level_start_room(); pos = pop_level_start_pos(); dir = (int8_t)~pop_level_start_dir(); /* Особое событие: старт с чекпойнта (do_startpos, seg003:0141). * Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles() * ниже, иначе рестарт её вернёт. */ chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint); if (chkp) { start_room = POP_CHKP_RESPAWN_ROOM; pos = POP_CHKP_RESPAWN_TILEPOS; dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную * и разворачивается seq_5 */ } /* Рестарт уровня = load_level() заново (play_level, seg003:57): в * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы — * разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и * убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */ pop_trob_reset(); pop_level_reset_tiles(); pop_level_reset_guards(); pop_guard_reset(); /* start_level: Guard.direction = none */ pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ /* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в * комнату-чекпойнт после респавна остался бы непройденным. */ if (chkp) pop_level_set_tile(POP_CHKP_CLEAR_ROOM, (uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL), TILE_EMPTY); /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы * ещё не тронутое состояние решётки. */ if (entry_pose == ENTRY_POSE_FALL) { /* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5, * col2, row0 её же типом и модификатором (в оригинале и то и другое * берётся из curr_* после get_tile). */ uint8_t tp = 2; /* row0*10 + col2 */ pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]); seq = SEQ_FALL; } else if (entry_pose == ENTRY_POSE_RUN) { seq = SEQ_RUN; } /* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть * открыта уже в первом кадре (в оригинале find_start_level_door стоит * между do_startpos и draw_level_first). */ find_start_level_door(start_room); enter_room(start_room); kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir); /* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м * уровне Кид ставится в комнату 17, а экран тут же переводится на * комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) — * он влетает в неё сверху, продолжая падение с шестого уровня. */ if (pop_current_level == FALLING_ENTRY_LEVEL && start_room == FALLING_ENTRY_ROOM) { uint8_t below = pop_room_link(start_room, LINK_DOWN); if (below) { Kid.y = (uint8_t)(Kid.y - 189); Kid.curr_row = pop_y_to_row((int16_t)Kid.y); enter_room(below); } } Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ /* play_level (seg003:106): have_sword = level >= have_sword_from_level * (=2) — начиная со второго уровня меч у Кида уже есть. */ pop_have_sword = (uint8_t)(pop_current_level >= 2); pop_kid_hp_reset(); } /* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- * * * Нужен, чтобы пройти все комнаты уровня и собрать список багов отрисовки, * включая недостижимые обычным путём. * * Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната * смотрелась в исходном состоянии». Убран: reset обнуляет room_seen, и * модификаторы перечитываются из уровня заново, то есть чит ОТМАТЫВАЕТ МИР * — открытые/закрытые ворота, выдвинутые пики, нажатые кнопки. Навигация * обязана только телепортировать; исходное состояние даёт перезапуск. */ uint8_t pop_dbg_roomnav(uint8_t dir) __banked { uint8_t r, i, c, pos = 20, any = 0xFF; /* Кид МЁРТВ: сначала честный рестарт уровня, потом телепорт. Навигация * только переставляет Кида (kid_init + сброс HP), то есть «оживляет» его * мимо start_level — а тот обязан вернуть тайлы, модификаторы и ОЧИСТИТЬ * слот соперника (seg003:100: Guard.direction = dir_56_none). Без этого * после смерти оставался живой скелет рядом с вернувшейся кучей костей. */ if (pop_kid_dead) pop_start_level(); r = cur_room; r = (dir == 1) ? (uint8_t)(r == 24 ? 1 : r + 1) : (uint8_t)(r == 1 ? 24 : r - 1); enter_room(r); /* Куда поставить Кида в новой комнате: пол С МАКСИМАЛЬНЫМ Y — сначала * НИЖНИЙ ряд (2), потом 1, и только потом 0 (пользователь 2026-08-11). * Раньше брался первый пол подряд, то есть почти всегда (0,0) — верхний * левый угол: это и «прилёт» в проём/ворота, и, на уровне 5, посадка * вплотную к тени спецсобытия, которая ждёт в колонке −1 ряда 0. Вместо * сцены кражи зелья начиналась схватка, которой в этой комнате быть не * должно. Снизу вверх нужно и самой навигации: в верхнем ряду пола чаще * нет, и Кид проваливается из комнаты сразу после телепорта. * * ИСКЛЮЧЕНИЕ — комната 14 уровня 7 (пользователь 2026-08-12): её проходят * СВЕРХУ ВНИЗ (с ряда 0 на ряд 2 через зацеп за кромки), и с нижнего ряда * этот спуск не проверить вовсе. Пол в ряду 0 там есть, так что Кид не * проваливается. * * В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его там нет — * любой проходимый tile_is_floor (seg001: всё, кроме пустоты, верха * колонны, верха двери, стены и «половинок» 26..29). */ { int8_t row, step = -1, first = 2; if (pop_current_level == GRAB_DOWN_LEVEL && r == GRAB_DOWN_ROOM) { first = 0; step = 1; } for (row = first; row >= 0 && row <= 2 && any == 0xFF; row = (int8_t)(row + step)) { uint8_t fallback = 0xFF; for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) { c = (uint8_t)(room_fg[i] & 0x1F); if (c == 1) { any = i; break; } if (fallback == 0xFF && c && c != 9 && c != 12 && c != 20 && !(c >= 26 && c <= 29)) fallback = i; } if (any == 0xFF) any = fallback; } } if (any != 0xFF) pos = any; kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0); pop_kid_hp_reset(); /* ВЫХОД ИЗ БОЯ (BUG-CHEAT-FIGHT-1). Телепорт с вынутым мечом заклинивал * управление намертво: pop_control при Kid.sword == SWORD_2_DRAWN уходит * в control_with_sword, а там единственный выход — ветка «убрать меч» на * кадре FRAME_171_STAND_WITH_SWORD. После kid_init(SEQ_STAND) кадр * обычный, стража в новой комнате нет, swordfight не запускается — и * диспетчер каждый кадр не делает НИЧЕГО. Это болезнь ЧИТА: в оригинале * телепорта между комнатами нет, попасть в режим боя без соперника * нечем. Меч в инвентаре (pop_have_sword) НЕ теряем — только состояние * схватки. */ Kid.sword = SWORD_0_SHEATHED; holding_sword = 0; offguard = 0; guard_refrac = 0; return 1; } /* ---- ОТЛАДКА: номер отрисованной комнаты в верхнем борте ------------- */ static uint8_t room_label_sig = 0xFF; /* последняя показанная комната */ static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */ void pop_dbg_room_label(uint8_t room) __banked { uint8_t prev = room_label_sig, i; room_label_sig = room; /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */ if (room != prev) room_label_redraw = 2; if (!room_label_redraw) return; /* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный знакогенератор * (_gfx_font_buf, 2 КБ статики в W2) и сажает кучу. Слева десятки, * справа единицы: || |||| = 24. */ setfillstyle(SOLID_FILL, BLACK); bar(100, 0, 175, 5); /* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная * (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */ setfillstyle(SOLID_FILL, 0x57); for (i = 0; i < (uint8_t)(room / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5); for (i = 0; i < (uint8_t)(room % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5); room_label_redraw--; } /* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) ----------------------- * * В оригинале никто не «поверх» по определению: и Кид, и страж попадают в * midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а * тайлы обходятся в фиксированном порядке — redraw_needed_tiles (seg008:1B06) * идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ — тот * поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или * выше, его тайл в обходе позже. Совпали тайлы — решает sort_curr_objs * (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже. * * Мы рисовали страж→Кид всегда, то есть Кид был поверх безусловно. * * Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 → (2 - row). */ #define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col)) /* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */ static int8_t tile_div_mod(int x) { int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */ int c = v / TILE_SIZEX; if (v % TILE_SIZEX < 0) --c; return (int8_t)c; } /* char_x_left из set_char_collision (seg006:0723) — левая граница габарита. * ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к * *_coll, а колонки char_col_left/right считает по этой. */ static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx, uint8_t flags, uint16_t w) { int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */ uint8_t fl = flags; if (dir < 0) fl ^= 0x80; /* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */ if (!(fl & 0x80) && e <= 57) e++; if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */ return e; } /* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа. * * Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил * сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ * колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому * бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров * бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих: * enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт * дал бы перекос в другую сторону. */ static void objtile_at_char(int8_t *row, int8_t *col, const pop_char_t *ch, int8_t dx, int8_t dy, uint8_t flags, uint16_t w) { int8_t r = ch->curr_row, c = ch->curr_col; if (ch->action == 1) { /* actions_1_run_jump */ int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy)); if (br == -1) br = 3; /* char_bottom_row */ r = br; c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w)); if (c < 0) c = 0; /* MAX(char_col_left, 0) */ } if ((ch->frame >= 135 && ch->frame < 149) || ch->action == 2 || ch->action == 3 || ch->action == 4 || ch->action == 6) c--; *row = r; *col = c; } /* 1 = страж рисуется ПОСЛЕ Кида (поверх). __banked: зовётся раз в кадр, * то есть один трамплин — а 500 Б кода в резиденте дороже. */ uint8_t guard_over_kid(void) __banked { int8_t kr, kc, gr, gc; int kk, gk; if (Guard.charid == 0 || Guard.room != cur_room) return 0; objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(), kid_cur_flags(), pop_cd[POP_CH_KID].fpw); objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy, pop_gframe.flags, pop_cd[POP_CH_OPP].fpw); kk = OBJ_ORDER_KEY(kr, kc); gk = OBJ_ORDER_KEY(gr, gc); if (gk != kk) return (uint8_t)(gk > kk); /* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */ return (uint8_t)((int)Guard.y + pop_gframe.dy > pop_cd[POP_CH_KID].fpy); } /* ---- Переворот уже нарисованного (зелье инверсии, уровень 9) --------- * * * Порт need_redraw_because_flipped (seg000:15E9 + 939/946). Оригинал * держит готовый offscreen и переворачивает его на выводе; у нас страниц * ровно две, рабочей третьей нет, поэтому переворачиваем то, что уже * нарисовано, — построчной копией акселератора (gfx_copy_page). * * Почему это законно и почему heal после не ломается: accel ЧИТАЕТ * ОЗУ-копию, значит копируется ЧИСТЫЙ ФОН без персонажей, а пишет банком * 0x50 — и в видео, и в копию приёмника. После двух проходов обе страницы * дабл-буфера содержат перевёрнутый фон в обеих плоскостях, и heal * восстанавливает уже его (memory accel_port_y_between_triggers, * регресс — tests/pageflip). * * Порядок: перевернуть в СКРЫТУЮ, показать её, скопировать оттуда во * вторую (теперь скрытую) — прямой копией. Так игрок видит результат * сразу, а не после второго прохода. * * Переворачиваем ТОЛЬКО игровое поле (POP_PLAYFIELD_H): полоса HP лежит * ниже него и в оригинале тоже не переворачивается. */ void pop_flip_screen(void) __banked { gfx_rect_t area; uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1); area.x = 0; area.y = POP_YOFF; area.w = 320; area.h = POP_PLAYFIELD_H; gfx_set_draw_page(back); gfx_copy_page(&area, GFX_COPY_VFLIP); /* источник — видимая страница */ gfx_set_visible_page(back); gfx_set_draw_page((uint8_t)(back ^ 1)); gfx_copy_page(&area, GFX_COPY_DIRECT); /* вторую страницу — прямой копией */ /* Всё, что рисуется поверх фона, надо положить заново: прошлые * прямоугольники персонажей относятся к СТАРОЙ (неперевёрнутой) * картинке, и heal по ним стёр бы не то место. */ pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0; pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0; pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0; pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0; POP_CD_TOUCH_ALL(); /* фон трогали целиком */ pop_hp_invalidate(); /* полосу HP перерисовать */ }