147cb185b1
MEM-COLD3, шаги 3-5. Кандидаты выбирались не по размеру, а по тому, КТО зовёт: если единственный вызывающий уже в банке, код едет к нему и трамплин не появляется вовсе. * pop_kdraw.c -> БАНК 4 (к pop_cdraw.c): клинок в руке (pop_sword_draw) и блит спрайта chtab_2 по id (pop_kid_img_blit). Оба звал ровно pop_cdraw, теперь вызовы прямые — контракт и условие корректности в _pop_kdraw.h. * pop_kboot.c -> БАНК 8: загрузка атласов Кида и страницы kid_data.bin (раз за игру). Дескриптор страницы — _pop_kid.h. * pop_guard_cold.c -> БАНК 8: страж идёт за Кидом + вход стража в комнату. Оба зовёт roomtest_cold.c из того же банка — снова без трамплина. * Из main в банк 8 уехали pop_boot (загрузка ресурсов, палитра, первый экран), pop_level_switch (смена уровня) и pop_shutdown. FIRST_LEVEL переехал в pop_tune.h — константа нужна обеим половинам цикла. _CODE 23005 -> 19861, куча 3525 -> 6669 Б. Банк 8 — 6.5 из 16 КБ. tests-host 6/6 (в наборы добавлены eng_pop_kboot и eng_pop_guard_cold).
653 lines
41 KiB
C
653 lines
41 KiB
C
/*
|
||
* roomtest_cold.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК
|
||
* (--bank 8=roomtest_cold.c, окно W3). Здесь всё, что отрабатывает раз на
|
||
* уровень или только по отладочной клавише; кадровый путь остался в
|
||
* roomtest.c. Зачем разделили и что осталось снаружи — см. roomtest_cold.h.
|
||
*
|
||
* Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого
|
||
* модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие
|
||
* __banked-модули через трамплины.
|
||
*/
|
||
#include <graphics.h>
|
||
#include <gfx.h>
|
||
#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 <sprite.h> /* gfx_copy_page */
|
||
#include "pop_tune.h"
|
||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||
#include <stdio.h> /* puts/printf — сообщения об ошибках загрузки */
|
||
#include <kbd_raw.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--;
|
||
}
|
||
|
||
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
|
||
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
|
||
* активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
|
||
/* side — куда ушёл КИД (0 left, 1 right, 2 up, 3 down); 0xFF = «не переход
|
||
* за Кидом» (старт уровня, респавн, чит-навигация). От него зависит ровно
|
||
* одно: пойдёт ли страж следом (pop_guard_follow, порт exit_room+
|
||
* follow_guard) или останется в прежней комнате. */
|
||
void enter_room_side(uint8_t room, uint8_t side) __banked
|
||
{
|
||
uint8_t pg;
|
||
/* Страж идёт за Кидом? — решаем ДО того, как что-либо загрузим: функции
|
||
* нужны и старая комната (Guard.room), и целевая. Пошёл — ни leave, ни
|
||
* enter звать нельзя: он уже перенесён и живёт дальше как есть. */
|
||
uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0);
|
||
if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */
|
||
uint8_t *room_mod;
|
||
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
|
||
pop_map_set(room_fg);
|
||
/* Соседей слева/справа карта забирает из уровня САМА (целиком, см.
|
||
* pop_map_set_edges) — отдельной копии в приложении нет. */
|
||
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
|
||
pop_room_link(room, 2), pop_room_link(room, 3));
|
||
/* Боковой переход: колонки сдвинулись ровно на 10 (x ∓140), флаги
|
||
* перекрытия при этом инвариантны — историю перенумеровываем, а не
|
||
* теряем. Без этого решётка шва не бампит на кадре входа и Кид с
|
||
* разбега проходит сквозь закрытые ворота (BUG-GATE-PASS-1). */
|
||
if (side == 0) pop_coll_shift(+10); /* ушёл ВЛЕВО: col += 10 */
|
||
else if (side == 1) pop_coll_shift(-10); /* ушёл ВПРАВО: col −= 10 */
|
||
pop_room_set_below(below_fg);
|
||
/* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика
|
||
* уровня, mod — живой room_modif (ленивая инициализация). */
|
||
{
|
||
uint8_t up = pop_room_link(room, 2), c;
|
||
if (up) {
|
||
uint8_t *um = pop_trob_modif(up);
|
||
pop_level_access_begin();
|
||
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
|
||
pop_level_access_end();
|
||
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
|
||
pop_room_set_above(above_fg, above_mod);
|
||
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
|
||
} else {
|
||
pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */
|
||
pop_map_set_above(0);
|
||
}
|
||
}
|
||
pop_map_set_room(room);
|
||
/* 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_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */
|
||
/* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»-
|
||
* уровнях забирает слот соперника себе: вернула 1 — стража поднимать
|
||
* НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда
|
||
* не пускает). Порядок ровно как в оригинале, не «сначала страж, потом
|
||
* заменим». */
|
||
if (!follow && !pop_check_shadow(room))
|
||
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
|
||
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
|
||
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
||
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
||
* а не по свежему backtable. Фаза P0. */
|
||
room_mod = pop_trob_modif(room);
|
||
/* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот,
|
||
* видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6).
|
||
* pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */
|
||
{
|
||
uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1);
|
||
if (lft) { uint8_t *m = pop_trob_modif(lft), i;
|
||
for (i = 0; i < 3; i++) {
|
||
lcol_bg[i] = m[9 + i * 10];
|
||
/* шов рисуется в обе страницы ниже — синхр. сигнатуру */
|
||
seam_sig[i] = seam_row_sig(m, lcol_fg, i);
|
||
} }
|
||
if (rgt) { uint8_t *m = pop_trob_modif(rgt);
|
||
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; }
|
||
}
|
||
seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */
|
||
/* ENTER-ROOM-FAST: комната рисуется ОДИН раз — в скрытую страницу, — и
|
||
* тут же показывается флипом; вторая страница дабл-буфера получает её
|
||
* КОПИЕЙ акселератора (gfx_copy_page, ~384 burst-строки против почти
|
||
* полумиллиона тактов на вторую отрисовку). Побочный эффект важнее
|
||
* тактов: игрок больше не видит, КАК рисуются тайлы — только готовый
|
||
* кадр.
|
||
*
|
||
* Копия идёт банком 0x50, то есть обновляет и VRAM, и ОЗУ-копию
|
||
* страницы-приёмника — иначе heal на ней восстанавливал бы фон прошлой
|
||
* комнаты (инвариант gfx_copy_page, регресс tests/pageflip).
|
||
*
|
||
* Видимая страница здесь МЕНЯЕТСЯ, поэтому главный цикл перечитывает её
|
||
* в начале каждого кадра (front = gfx_get_visible_page()), а не ведёт
|
||
* свой счёт. */
|
||
{
|
||
gfx_rect_t full;
|
||
pg = (uint8_t)(gfx_get_visible_page() ^ 1); /* скрытая */
|
||
gfx_set_draw_page(pg);
|
||
setfillstyle(SOLID_FILL, BLACK);
|
||
bar(0, 0, 319, 255);
|
||
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
|
||
gfx_set_visible_page(pg); /* показать готовое */
|
||
gfx_set_draw_page((uint8_t)(pg ^ 1));
|
||
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
|
||
gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */
|
||
}
|
||
cur_room = room;
|
||
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
|
||
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
|
||
}
|
||
|
||
/* Вход в комнату БЕЗ перехода за Кидом: старт уровня, респавн, чит-обход.
|
||
* Страж такой смены не «видит» — он остаётся в своей комнате. */
|
||
void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); }
|
||
|
||
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
|
||
* (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и
|
||
* для запуска, и для рестарта после смерти / выпадения из уровня: в
|
||
* оригинале это буквально один и тот же путь (play_level_2 зовёт
|
||
* start_level заново), поэтому и у нас не три копии четырёх строк.
|
||
*
|
||
* Позиция берётся ИЗ ДАННЫХ уровня (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(). */
|
||
#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 */
|
||
|
||
|
||
/* ---- Порядок «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 перерисовать */
|
||
}
|
||
|
||
/* ---- Запуск, смена уровня, завершение ------------------------------- *
|
||
* Три куска главного цикла, которые отрабатывают раз за игру (или раз на
|
||
* уровень). В резиденте от них оставались только вызовы — сам код здесь.
|
||
*/
|
||
|
||
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();
|
||
}
|