Files
Sprinter-SDCC/applications/PoP/roomtest/roomtest_cold.c
T
snark13 b1ef48bd16 L9-INVERT: состояние переворота, зелье типа 4 и pop_flip_screen
Шаг II.1 плана.  pop_upside/pop_upside_dirty (порт upside_down и
need_redraw_because_flipped), ветка case 4 в proc_get_object (только тоггл —
ни вспышки, ни урона, как в оригинале), сброс стартом уровня и смертью Кида.

pop_flip_screen (банк 8) переворачивает уже нарисованное двумя копиями
акселератора: VFLIP в скрытую страницу, показ, DIRECT во вторую; затем
инвалидация слотов персонажей, метки фона и полосы HP.

Чит-клавиша U — порт seg000:0793 (в оригинале переворот тоже на чите): без
неё эффект не проверить, чит-телепорт до зелья в комнате 7 не дотягивается.

Проверено в MAME на уровне 9: комната переворачивается целиком за кадр,
повторное нажатие возвращает.  Персонажи и факелы пока не зеркалятся —
это шаги II.3/II.4.

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

303 lines
19 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--;
}
/* ---- Переворот уже нарисованного (зелье инверсии, уровень 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 перерисовать */
}