Files
Sprinter-SDCC/applications/PoP/roomtest/pop_level_cold.c
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:58:36 +03:00

327 lines
18 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.
/*
* pop_level_cold.c — ХОЛОДНАЯ половина уровня (БАНК 8). Контракт и деление
* половин — в _pop_level.h.
*
* Здесь всё, что отрабатывает раз на уровень (чтение файла res200N.bin,
* рестарт тайлов/стражей, потабличные различия) или раз на комнату (разбор
* комнаты в рабочие массивы, посадочный ряд колонки, страж комнаты). Цена
* выноса — один трамплин на вызов; в кадре этих вызовов ноль.
*
* Формат — порт blueprnt DAT 1.0 (Table 6 POP-DAT).
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
#include "pop_level.h"
#include "_pop_level.h"
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
void pop_level_free(void) __banked
{
if (pop_lvl_ok) { mem_free_block(pop_lvl_blk); pop_lvl_ok = 0; }
}
/* ---- Потабличные различия уровней ------------------------------------ */
/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = demo).
* Хвоста level 15 здесь намеренно нет: настоящий маршрут заканчивается 14.
* Так диапазон данных и диапазон загрузчика не могут разъехаться. */
static const uint8_t tbl_entry_pose[POP_LEVEL_LAST + 1] =
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0 };
static const uint8_t tbl_guard_hp[POP_LEVEL_LAST + 1] =
{ 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0 };
static const int8_t tbl_guard_type[POP_LEVEL_LAST + 1] =
{ 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1 };
/* tbl_level_type (SDLPoP data.h:840): 0 — подземелье, 1 — дворец. По нему
* расходятся не только атласы, но и ветки отрисовки (seg008), кладка стены
* (wall_pattern) и модификатор пустой клетки от упавшей плиты
* (remove_loose, seg007:0EB8). */
static const uint8_t tbl_level_type[POP_LEVEL_LAST + 1] =
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1 };
uint8_t pop_level_entry_pose(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_entry_pose[n] : 0; }
uint8_t pop_level_guard_hp(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_guard_hp[n] : 3; }
int8_t pop_level_guard_type(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_guard_type[n] : -1; }
uint8_t pop_level_tileset(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_level_type[n] : 0; }
uint8_t pop_level_type(void) __banked { return pop_level_tileset(pop_current_level); }
static int level_read_file(const char *path)
{
uint8_t blk, page;
uint16_t i;
int n;
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
gfx_w0_page_prepare(page);
gfx_w0_map(page);
for (i=0; i<256; i++) {
pop_dl1[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKLOC+i];
pop_dl2[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKMAP+i];
}
for (i=0; i<24u*ROOM_TILES; i++)
((uint8_t *)LVL_PRISTINE_OFF)[i]=
((const uint8_t *)(LVL_DATA_OFF+BP_FG))[i];
gfx_w0_unmap();
pop_lvl_blk=blk; pop_lvl_page=page; pop_lvl_ok=1;
pop_gstate_init();
return 0;
}
int pop_level_load_num(uint8_t n) __banked
{
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
* резидент W1 форматтер, которого здесь больше негде применить. */
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
if (n > POP_LEVEL_LAST) return -1;
pri[12] = alt[8] = (char)('0' + n / 10);
pri[13] = alt[9] = (char)('0' + n % 10);
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
* на текущем уровне вместо падения в пустой уровень. */
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
return -1;
}
if (saved_ok) mem_free_block(saved_blk);
pop_current_level = n;
return 0;
}
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg) __banked
{
const uint8_t *rfg, *rbg, *lnk;
uint8_t left, right, down, i;
if (!pop_lvl_ok || room < 1 || room > 24) return -1;
gfx_w0_map(pop_lvl_page);
rfg = room_fg_ptr(room);
rbg = room_bg_ptr(room);
lnk = (const uint8_t *)(LVL_DATA_OFF + BP_LINKS) + (uint16_t)(room - 1) * 4;
for (i = 0; i < ROOM_TILES; i++) {
fg[i] = (uint8_t)(rfg[i] & 0x1F); /* тип тайла (верх. биты — модиф., пока не нужны) */
bg[i] = rbg[i]; /* backtable как есть */
}
left = lnk[0]; /* L */
right = lnk[1]; /* R */
down = lnk[3]; /* D */
/* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0),
* а в [3..5] — ПРЕДпоследний (col8). Вторая колонка нужна коллизии: Kid,
* стоящий В шве (curr_col = −1), смотрит вперёд на колонку −2, и без неё
* там оказывалась мнимая стена (get_tile → TILE_WALL), которая выпихивала
* его сквозь решётку. Оригинал резолвит любую колонку через
* find_room_of_tile (seg006:005D). */
if (left >= 1 && left <= 24) {
const uint8_t *lf = room_fg_ptr(left);
const uint8_t *lb = room_bg_ptr(left);
for (i = 0; i < 3; i++) {
lcol_fg[i] = (uint8_t)(lf[i * 10 + 9] & 0x1F);
lcol_fg[3 + i] = (uint8_t)(lf[i * 10 + 8] & 0x1F);
lcol_bg[i] = lb[i * 10 + 9];
}
} else {
for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_fg[3 + i] = 20; lcol_bg[i] = 0; }
}
/* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9:
* коллизия на правом шве + переход вправо) */
if (right >= 1 && right <= 24) {
const uint8_t *rf = room_fg_ptr(right);
const uint8_t *rb = room_bg_ptr(right);
for (i = 0; i < 3; i++) {
rcol_fg[i] = (uint8_t)(rf[i * 10 + 0] & 0x1F);
rcol_fg[3 + i] = (uint8_t)(rf[i * 10 + 1] & 0x1F); /* колонка 11 (см. выше) */
rcol_bg[i] = rb[i * 10 + 0];
}
} else {
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
}
/* belowrow: верхний ряд (row0) комнаты снизу — ОДИННАДЦАТЬ байт.
* [0..9] — колонки 0..9 комнаты снизу; [10] — тайл (0,9) комнаты
* ПО ДИАГОНАЛИ СНИЗУ-СЛЕВА (room_BL у оригинала). Одиннадцатый нужен
* тайлу (2,0): его «сосед снизу-слева» — это колонка −1 ряда 3, то есть
* чужая комната, и load_rowbelow (seg008:368) резолвит её именно так,
* с дефолтом «стена» только когда такой комнаты НЕТ. Раньше мы клали
* туда стену БЕЗУСЛОВНО, и в пустом углу рисовался клин кладки
* (BUG-SEAM-WEDGE-1, ур. 3 комн. 18). */
if (down >= 1 && down <= 24) {
const uint8_t *df = room_fg_ptr(down);
uint8_t dl = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(down - 1) * 4];
for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F);
below_fg[10] = (dl >= 1 && dl <= 24)
? (uint8_t)(room_fg_ptr(dl)[9] & 0x1F)
: 20; /* кромка уровня */
} else {
/* Комнаты снизу НЕТ. Оригинал (load_rowbelow, seg008:368) в этом
* случае подставляет РАЗНЫЕ кромки: колонкам 1..9 — `tiles_0_empty`,
* и только левому краю (колонка 0, тайл из room_BL) — `tiles_20_wall`.
* У нас тут стояла стена на всё, и её грань (topright стены) лезла
* жёлтыми треугольниками под пустые тайлы нижнего ряда — видно в
* шахте падения, уровень 6 комната 3 (BUG-BELOWROW-WALL). */
for (i = 0; i < 10; i++) below_fg[i] = 0; /* tiles_0_empty */
below_fg[10] = 20; /* левая кромка уровня */
}
gfx_w0_unmap();
return 0;
}
int8_t pop_room_col_landing(uint8_t room, uint8_t col) __banked
{
const uint8_t *rf;
int8_t row, res = -1;
if (!pop_lvl_ok || room < 1 || room > 24 || col > 9) return -1;
gfx_w0_map(pop_lvl_page);
rf = room_fg_ptr(room);
for (row = 0; row < 3; row++) { /* кусок пролетает пустые ряды */
uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
if (t == 0) continue; /* empty — дальше вниз */
/* Набор площадок — дословно switch из loose_land (seg007:11E8):
* пол, пика, обе кнопки, зелье и оба факела. На всём остальном
* (стена, ворота, колонна) кусок просто исчезает. Раньше здесь
* стоял только пол, и плита, падавшая на КНОПКУ в комнате снизу,
* пропадала бесследно: ни щебня, ни нажатия (уровень 4, плита
* 16(1,1) -> кнопка 17(0,1), открывающая ворота 23(0,9)). */
if (t == 1 || t == 2 || t == 6 || t == 10 ||
t == 15 || t == 19 || t == 30) res = row;
break; /* первый не-пустой тайл — стоп */
}
gfx_w0_unmap();
return res;
}
/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть
* в исходное состояние ВСЁ, что игра правит в данных уровня.
*
* Это foretable (из эталонной копии — сами данные мы правим на месте) И
* ТАБЛИЦА СВЯЗЕЙ. Про вторую легко забыть, потому что мутирует в ней ровно
* одно поле — таймер нажатия кнопки в младших битах LINKMAP, — но забыть
* дорого: `pop_tile_code_drawn` рисует кнопку НАЖАТОЙ, пока таймер > 1, и
* ненулевой остаток показывает её нажатой с первого кадра уровня.
*
* Оставить такой остаток особенно легко из died_on_button: он ставит таймер
* в 5 и заводит trob кнопки, но тайл к этому моменту УЖЕ пол — trob умирает
* в первом же обходе (`default: type = -1`), не успев ни разу уменьшить
* таймер. У оригинала ровно то же самое, просто там load_level перечитывает
* весь файл целиком, и вопроса не возникает (найдено пользователем
* 2026-08-18: уровень 7, комната 3, кнопка (2,1) после первого же респавна).
*
* LINKMAP в странице уровня остаётся ЭТАЛОННЫМ: игра правит только
* ОЗУ-копию pop_dl2 (pop_doorlink2_set), обратно в страницу не пишет никто —
* поэтому отдельная pristine-копия, как у foretable, тут не нужна. */
void pop_level_reset_tiles(void) __banked
{
uint16_t i;
uint8_t *dst;
const uint8_t *src;
if (!pop_lvl_ok) return;
gfx_w0_map(pop_lvl_page);
dst = (uint8_t *)(LVL_DATA_OFF + BP_FG);
src = (const uint8_t *)LVL_PRISTINE_OFF;
for (i = 0; i < 24u * ROOM_TILES; i++) dst[i] = src[i];
src = (const uint8_t *)(LVL_DATA_OFF + BP_LINKMAP);
for (i = 0; i < 256; i++) pop_dl2[i] = src[i];
gfx_w0_unmap();
}
/* Рестарт уровня, часть вторая (тот же load_level + pos_guards, seg003:83):
* стражи — такая же часть данных уровня, как тайлы, и перезагрузка файла
* возвращает их всех. Убитый до смерти Кида страж после respawn снова жив
* и стоит на исходном тайле; забывается и запомненная поза трупа
* (guards_seq_hi = 0 -> «поднять стандартной стойкой»). */
void pop_level_reset_guards(void) __banked
{
if (!pop_lvl_ok) return;
pop_gstate_init();
}
void pop_gstate_init(void) __banked
{
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
uint8_t r;
gfx_w0_map(pop_lvl_page);
for (r = 0; r < 24; r++) {
uint8_t *g = pop_gstate + (uint16_t)r * 6;
g[GS_TILE] = p[BP_GTILE + r];
g[GS_DIR] = p[BP_GDIR + r];
g[GS_X] = p[BP_GX + r];
g[GS_SKILL] = p[BP_GSKILL + r];
/* pos_guards (seg003:0913), сразу после load_level: X стража
* ПЕРЕСЧИТЫВАЕТСЯ из колонки тайла, а то, что лежит в файле, оригинал
* выбрасывает (в комнате 24 уровня 8 там, например, 255). Дальше X
* владеет leave_guard, и именно она — единственный носитель КОЛОНКИ:
* в guards_tile leave_guard кладёт get_tilepos(0, row), то есть
* колонку обнуляет намеренно. */
if (g[GS_TILE] < 30)
g[GS_X] = (uint8_t)(pop_x_bump[(g[GS_TILE] % 10) + FIRST_ONSCREEN_COLUMN]
+ TILE_SIZEX);
/* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
* комнатах (оригинал чистит их в reset_level_unused_fields).
* Осмысленное значение туда кладёт только leave_guard, поэтому
* стартуем нулями: 0 = «поднимать стандартной стойкой».
* Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
g[GS_SEQL] = 0;
g[GS_SEQH] = 0;
}
gfx_w0_unmap();
}
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
* колонки (pos_guards), поэтому guards_x из файла не читаем. */
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill) __banked
{
const uint8_t *g;
uint8_t t;
if (!pop_lvl_ok || room < 1 || room > 24) return 0;
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
g = pop_gstate + (uint16_t)(room - 1) * 6;
t = g[GS_TILE];
if (t < 30) {
*tile = t;
*dir = (int8_t)g[GS_DIR];
*skill = g[GS_SKILL];
gfx_w0_map(pop_lvl_page);
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
gfx_w0_unmap();
}
return (uint8_t)(t < 30);
}
/* ---- Стартовая позиция уровня ---------------------------------------- */
static uint8_t start_byte(uint16_t off)
{
uint8_t v;
if (!pop_lvl_ok) return 0;
gfx_w0_map(pop_lvl_page);
v = ((const uint8_t *)(LVL_DATA_OFF))[off];
gfx_w0_unmap();
return v;
}
uint8_t pop_level_start_room(void) __banked { return start_byte(BP_START + 0); }
uint8_t pop_level_start_pos(void) __banked { return start_byte(BP_START + 1); }
int8_t pop_level_start_dir(void) __banked { return (int8_t)start_byte(BP_START + 2); }