Files
Sprinter-SDCC/applications/PoP/roomtest/roomtest_cold.c
T
snark13 7c1ded340c L9-INVERT: зеркальный слой фона + вход в комнату в банк 8
II.3: pop_upside проведён в единственную точку тайлового слоя (pop_blit_b /
blit_b_clip): позиция через FLIP_TOP, блит — vflip-двойник.  Этим зеркалятся
полная отрисовка комнаты, точечные редрои, trob-анимации, fore-слой и кладка.
В клипованном пути обрезка сверху экрана съедает нижние строки источника,
поэтому кусок пересчитывается как sy' = h - sy - dh.

MEM-COLD2 п.1: enter_room_side/enter_room — в банк 8 (куча 450 -> 1231 Б).
Рабочие массивы комнаты остались в _DATA, в банк уехал только код.

Проверено в MAME (уровень 9, чит U): комната перевёрнута целиком, пол
вверху, дверь и кладка вверх ногами.  tests-host 6/6, size-check OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:43:22 +03:00

532 lines
34 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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"
#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 перерисовать */
}