47a4b084c4
Эта сессия (меню + текст в служебных полосах):
* Меню: рамка выделения считается от силуэта текста (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>
368 lines
23 KiB
Markdown
368 lines
23 KiB
Markdown
# От roomtest к полноценной игре — сценарий и оболочка
|
||
|
||
Статус: **частично реализовано; аудит обновлён 2026-08-24**. Ранее пометка
|
||
«завершены FG0–FG12» была неверной: для многих этапов уже есть код и
|
||
host-тесты, но их критерии приёмки на Sprinter ещё не выполнены. Фактический
|
||
статус каждого FG приведён в [§14](#14-этапы-реализации).
|
||
|
||
Этот документ описывает превращение текущего игрового цикла
|
||
`roomtest` в законченную игру: заставка, интро, демонстрационный уровень,
|
||
сцены между уровнями, таймер, финал и Hall of Fame. План меню и постоянных
|
||
настроек вынесен в [`menu_settings_plan.md`](menu_settings_plan.md),
|
||
детальный план QuickSave — в [`quicksave_plan.md`](quicksave_plan.md).
|
||
|
||
## 1. Зафиксированный scope
|
||
|
||
- Целевая последовательность — оригинальная SDLPoP/DOS PoP с уровнями
|
||
**1..14**. Уровень 14 — скрытая финальная часть после Джаффара: его номер
|
||
игроку не показывается, победа наступает в комнате 5.
|
||
- Уровень **15 удаляется полностью**: не пакуется на HDD, не загружается,
|
||
отсутствует в переходах, читах и UI; специальная логика potions/copy
|
||
protection level удаляется.
|
||
- Уровень **0** остаётся только демонстрационным (attract mode), а не частью
|
||
новой игры.
|
||
- Title sequence повторяет SDLPoP. Перед ней допускается отдельный
|
||
пропускаемый экран с информацией о Sprinter-сборке.
|
||
- Первая версия использует текущий профиль поведения `VANILLA`. Сейчас это
|
||
означает **существующую реализацию roomtest**, включая уже встроенные
|
||
исправления. Аудит и разведение `VANILLA/ENHANCED` — будущая задача.
|
||
- Программа работает **только с HDD**. Варианты без сохранения для floppy не
|
||
проектируются.
|
||
- Моды и выбор levelset в этот план не входят.
|
||
|
||
## 2. Что делает SDLPoP
|
||
|
||
Источники истины в локальном SDLPoP:
|
||
|
||
- `src/seg000.c`: `start_game()`, `show_title()`, demo mode, общий кадр,
|
||
проверка финала;
|
||
- `src/seg003.c`: `init_game()`, `play_level()`, `play_level_2()`;
|
||
- `src/seg001.c`: cutscene engine, `pv_scene()`, сцены 2/4/6/8/9/12,
|
||
`time_expired()`, `end_sequence()` и Hall of Fame;
|
||
- `src/data.h`: `tbl_cutscenes`, параметры уровня 0, win level/room;
|
||
- `data/TITLE`, `data/PV`, `data/LEVELS/res2000.bin`: ресурсы оболочки.
|
||
|
||
Штатный маршрут:
|
||
|
||
```text
|
||
boot
|
||
-> title / story screens
|
||
-> Princess + Jaffar intro
|
||
-> credits / Hall of Fame
|
||
-> demo level 0
|
||
-> title или новая игра
|
||
-> levels 1..14
|
||
before 2 -> princess cutscene
|
||
before 4 -> princess cutscene
|
||
before 6 -> princess cutscene
|
||
before 8 -> princess + mouse
|
||
before 9 -> princess + mouse
|
||
before 12 -> scene selected by remaining time
|
||
-> level 14, room 5
|
||
-> embrace + mouse
|
||
-> ending text/music
|
||
-> Hall of Fame
|
||
-> title
|
||
```
|
||
|
||
Кроме уровней, здесь есть глобальный 60-минутный таймер, сцена истечения
|
||
времени, пропуск сцен клавишей, fade/flash, ожидание музыки и возврат в
|
||
attract loop после демо или финала.
|
||
|
||
## 3. Текущее состояние roomtest
|
||
|
||
Уже реализованы игровой кадр, комнаты, уровни, тайлсеты, Kid/Guard/Shadow,
|
||
Джаффар, специальные события, checkpoint, переходы уровней, бесшовный
|
||
выход 12-го уровня, перенос максимального HP и звуковые эффекты.
|
||
|
||
Поверх игрового цикла уже добавлены автомат оболочки, title/story, demo
|
||
уровень 0, global timer, сценарный интерпретатор, level-flow, ending и Hall
|
||
of Fame. Полный маршрут также собирается в HDD-образ.
|
||
|
||
Однако это **не означает готовность оболочки**. На момент аудита остаются
|
||
существенные незакрытые места:
|
||
|
||
- lifecycle палитр: gameplay-переходы используют чёрный барьер без fade;
|
||
cold start и полный набор dungeon/palace переходов ещё не прошли приёмку;
|
||
- PV intro Princess/Jaffar уже покадровый (актёры, факелы, звёзды, часы,
|
||
молния и foreground-колонна); сцены перед 2/4/6 и длинной веткой 12
|
||
анимируют факелы, звёзды и песок, а сцены 8/9 и короткая ветка 12 пока
|
||
используют статические позы с исходной длительностью;
|
||
- demo отображается с игровой палитрой, проходит второй разворот/зацеп и
|
||
доходит до боя; после смерти Кида корректно завершает цикл;
|
||
- time-expired, ending и Hall of Fame имеют маршрут и реализацию UI, но не
|
||
прошли сквозную MAME-проверку вместе с ресурсами и возвратом к title;
|
||
- нет полного регресса EMM/FD для каждого перехода состояния.
|
||
|
||
## 4. Архитектура: автомат состояний приложения
|
||
|
||
Нельзя наращивать все режимы условиями внутри кадрового цикла. Текущий
|
||
цикл должен стать реализацией одного состояния `PLAYING`:
|
||
|
||
```text
|
||
BOOT -> BUILD_INFO -> TITLE -> INTRO -> DEMO
|
||
| |
|
||
+---- NEW_GAME <-+
|
||
|
||
NEW_GAME -> LEVEL_LOAD -> PLAYING <-> PAUSE_MENU
|
||
|
|
||
+-> CUTSCENE -> LEVEL_LOAD
|
||
+-> TIME_EXPIRED -> TITLE
|
||
+-> ENDING -> HALL_OF_FAME -> TITLE
|
||
```
|
||
|
||
Минимальный контекст оболочки:
|
||
|
||
```c
|
||
typedef enum {
|
||
POP_APP_BOOT,
|
||
POP_APP_BUILD_INFO,
|
||
POP_APP_TITLE,
|
||
POP_APP_INTRO,
|
||
POP_APP_DEMO,
|
||
POP_APP_LEVEL_LOAD,
|
||
POP_APP_PLAYING,
|
||
POP_APP_PAUSE_MENU,
|
||
POP_APP_CUTSCENE,
|
||
POP_APP_TIME_EXPIRED,
|
||
POP_APP_ENDING,
|
||
POP_APP_HALL_OF_FAME,
|
||
POP_APP_QUIT
|
||
} pop_app_state_t;
|
||
```
|
||
|
||
Переходы задаются результатом состояния, а не прямыми рекурсивными
|
||
вызовами наподобие SDLPoP `start_game()`/`longjmp()`. На Z80 это проще для
|
||
стека и позволяет освобождать ресурсы каждого режима в одном месте.
|
||
|
||
## 5. Ресурсная модель
|
||
|
||
Title и cutscene-ресурсы нельзя постоянно держать рядом с игровыми
|
||
атласами. Для каждого состояния нужен явный lifecycle:
|
||
|
||
```text
|
||
enter: pause sound -> unload incompatible set -> load set -> apply palette
|
||
run: process input/timer/animation
|
||
leave: stop sound -> release EMM pages -> clear transient state
|
||
```
|
||
|
||
Новые группы HDD:
|
||
|
||
```text
|
||
TITLE\ title/story images, palette, optional build-screen assets
|
||
PV\ princess room, Princess/Jaffar/mouse frames, palettes
|
||
MUSIC\ intro, cutscene and ending tracks/samples
|
||
LEVELS\ res2000..res2014.bin
|
||
```
|
||
|
||
Конкретный формат атласов выбирает упаковщик. Runtime не должен разбирать
|
||
PNG/DAT: как и игровые спрайты, он получает подготовленные `.atl`/`.bin`.
|
||
|
||
## 6. Экран Sprinter build
|
||
|
||
Отдельное состояние перед оригинальной заставкой:
|
||
|
||
```text
|
||
PRINCE OF PERSIA
|
||
SPRINTER SP2000 BUILD
|
||
version / date / build id
|
||
```
|
||
|
||
Требования:
|
||
|
||
- пропускается любой клавишей;
|
||
- выключается в Settings;
|
||
- не запускает музыку оригинального title и не меняет её тайминги;
|
||
- данные версии генерируются сборкой, а не правятся вручную в C;
|
||
- отсутствие экрана приводит прямо к `TITLE`.
|
||
|
||
## 7. Title и текстовая подсистема
|
||
|
||
Порядок переносится из `show_title()`:
|
||
|
||
1. основной титульный экран;
|
||
2. Presents;
|
||
3. название игры и Jordan Mechner;
|
||
4. story frame / “In the absence…”;
|
||
5. intro Princess + Jaffar;
|
||
6. story “Marry Jaffar…”;
|
||
7. credits;
|
||
8. Hall of Fame, если таблица непуста;
|
||
9. demo level 0.
|
||
|
||
Нужны общие примитивы: загрузить full-screen image, вывести строку,
|
||
показать экран заданное время, transition left-to-right, fade in/out,
|
||
прервать ожидание клавишей. Текст и меню должны использовать один renderer.
|
||
|
||
Критерий: последовательность и музыкальные точки совпадают с SDLPoP;
|
||
Sprinter build screen не сдвигает оригинальный soundtrack.
|
||
|
||
## 8. Demo level 0
|
||
|
||
- Добавить на HDD `res2000.bin`.
|
||
- Загружать уровень обычным loader, но выставлять demo HP и demo mode.
|
||
- Воспроизводить `demo_moves` как синтетический источник `control_*`.
|
||
- Пользовательский ввод прерывает демо и начинает новую игру.
|
||
- Достижение demo end room (у SDLPoP — 24), смерть или конец скрипта
|
||
возвращают в `TITLE`.
|
||
- Pause menu, QuickSave и cheats в demo недоступны.
|
||
- RNG демо и начальное состояние должны быть детерминированы.
|
||
|
||
Критерий: без ввода attract loop не требует перезапуска процесса;
|
||
title -> demo -> title повторяется неограниченно.
|
||
|
||
## 9. Глобальный таймер
|
||
|
||
Состояние: минуты, тики и флаг показа. Таймер создаётся при New Game,
|
||
переносится между уровнями и входит в QuickSave.
|
||
|
||
Правила `VANILLA`:
|
||
|
||
- на pause menu, загрузке HDD, QuickSave/QuickLoad время не идёт;
|
||
- игровые тики следуют темпу логического кадра, а не частоте render loop;
|
||
- поведение во время level-end sound и cutscenes сверяется буквально с
|
||
SDLPoP;
|
||
- после Джаффара/на финальном уровне время не должно вызвать поражение;
|
||
- ноль времени переводит приложение в `TIME_EXPIRED`.
|
||
|
||
Критерий: одинаковый игровой отрезок в NORMAL даёт то же уменьшение времени,
|
||
что SDLPoP; сохранение/загрузка не добавляет и не отнимает тики.
|
||
|
||
Реализация FG4 живёт одним модулем `roomtest/pop_timer.c` в bank 9:
|
||
`60:719`, 720 тиков на минуту, счёт только в живом игровом кадре. Settings
|
||
хранит `TIME LIMIT: 60 MIN / UNLIMITED` в `POP.CFG`; старый семибайтный v1
|
||
payload по-прежнему читается как `60 MIN`. Читы таймера повторяют SDLPoP,
|
||
но из-за занятого `+/-` используют F7 (−1 минута, не ниже одной) и F8
|
||
(+1 минута). Состояние входит в QuickSave v4.
|
||
|
||
## 10. Cutscene engine
|
||
|
||
Сцены SDLPoP состоят из небольшого набора повторяемых команд. Вместо набора
|
||
крупных C-функций нужен компактный интерпретатор:
|
||
|
||
```text
|
||
SET_ACTOR actor
|
||
SET_POS x,y,dir
|
||
START_SEQ seq
|
||
WAIT_FRAMES n
|
||
PLAY_SOUND id
|
||
WAIT_SOUND
|
||
SET_HOURGLASS frame
|
||
SET_SAND state
|
||
FLASH color,frames
|
||
FADE_IN / FADE_OUT
|
||
CLEAR_ACTOR actor
|
||
END
|
||
```
|
||
|
||
Скрипты — `const` в холодном банке или подготовленный бинарный ресурс.
|
||
Interpreter обязан:
|
||
|
||
- исполнять один шаг/кадр без блокирующих длинных циклов;
|
||
- поддерживать пропуск сцены;
|
||
- при пропуске выполнять cleanup и выходить в заранее заданное состояние;
|
||
- освобождать PV-ресурсы перед загрузкой игрового тайлсета;
|
||
- не разрешать pause menu/QuickSave внутри сцены.
|
||
|
||
Порядок переноса: intro, 2/6, 4, 8, 9, 12, time expired, ending. Сцена 12
|
||
выбирает короткий или обычный вариант по остатку времени.
|
||
|
||
## 11. Переходы между уровнями
|
||
|
||
Таблица сценария должна быть отдельна от таблиц механики уровня:
|
||
|
||
```c
|
||
typedef struct {
|
||
uint8_t level;
|
||
uint8_t pre_cutscene;
|
||
uint8_t show_level_number;
|
||
uint8_t ending_rule;
|
||
} pop_level_flow_t;
|
||
```
|
||
|
||
Особые правила:
|
||
|
||
- New Game начинает уровень 1;
|
||
- перед 2/4/6/8/9/12 запускается сцена;
|
||
- 12 -> 13 остаётся бесшовным;
|
||
- после победы над Джаффаром переход идёт в 14;
|
||
- номер 14 не показывается;
|
||
- вход в комнату 5 уровня 14 переводит в `ENDING`;
|
||
- значения больше 14 недопустимы и дают диагностическую ошибку, а не
|
||
попытку открыть файл.
|
||
|
||
## 12. Ending и Hall of Fame
|
||
|
||
Ending:
|
||
|
||
1. загрузить PV-набор;
|
||
2. встреча Kid и Princess;
|
||
3. объятие;
|
||
4. появление мыши;
|
||
5. ending music;
|
||
6. финальные story/title экраны;
|
||
7. переход в Hall of Fame.
|
||
|
||
Hall of Fame хранится на HDD в отдельном версионированном `POP.HOF`.
|
||
Сохраняются имя и результат; ввод имени использует тот же текстовый/UI слой.
|
||
Повреждённый или неизвестный формат означает пустую таблицу, но не мешает
|
||
запуску игры. После показа — возврат в `TITLE`.
|
||
|
||
## 13. Удаление уровня 15
|
||
|
||
Отдельный ранний этап, чтобы новый flow не наследовал лишний маршрут:
|
||
|
||
- убрать `res2015.bin` из `LVL_NUMS` и HDD image;
|
||
- заменить последний игровой уровень на 14;
|
||
- остановить Shift+L и прочую навигацию на 14;
|
||
- удалить `POP_POTIONS_LEVEL` и специальный половинный урон синих зелий;
|
||
- исключить copy protection из конфигурации и меню;
|
||
- добавить тест: после уровня 14 приложение входит в ending и никогда не
|
||
запрашивает `res2015.bin`.
|
||
|
||
## 14. Этапы реализации
|
||
|
||
Легенда аудита: **✓** — критерий этапа закрыт; **~** — код существует, но
|
||
критерий приёмки ещё не закрыт; **○** — не начат. Статус отражает состояние
|
||
исходников и последней MAME-проверки на 2026-08-24, а не только наличие
|
||
модуля в bank 9.
|
||
|
||
| этап | статус | результат и фактическое состояние | критерий приёмки |
|
||
|---|---|---|---|
|
||
| **FG0** | ✓ | `POP_LEVEL_LAST=14`, HDD содержит `res2000..res2014`; `t_flow` отвергает 15 | HDD не содержит res2015; переход выше 14 невозможен |
|
||
| **FG1** | ~ | автомат `pop_app` и `t_app` реализованы; сквозной ресурсный lifecycle и контроль EMM/FD ещё не измерены | старт/рестарт/выход проходят без рекурсии и утечки EMM |
|
||
| **FG2** | ✓ | QuickSave/QuickLoad с `POP.SAV` и `POP.BAK`; отдельно проверен в MAME 2026-08-22 | критерии `quicksave_plan.md`, включая POP.BAK |
|
||
| **FG3** | ✓ | pause menu, Settings, подтверждения и двойной буфер реализованы; меню проверялось в MAME; добавлены SDLPoP-звуки навигации и защита CBL вокруг полного redraw/файловых операций | Resume/Save/Load/Restart/Settings/Quit работают |
|
||
| **FG4** | ~ | `pop_timer`, настройка unlimited, F7/F8 и состояние QuickSave реализованы; есть host-тест, но нет буквального сравнения темпа со SDLPoP на всех переходах | совпадение с SDLPoP и корректный save/load |
|
||
| **FG5** | ~ | text/full-screen/fade примитивы есть; для входа в первый уровень и границ уровней выбран мгновенный чёрный барьер без fade: CBL и яркая новая палитра включаются только после подготовки обеих страниц; Level 1 проверен в MAME | тестовые экраны и переходы на Sprinter |
|
||
| **FG6** | ~ | title-ресурсы и порядок кадров реализованы; Enter на title и Esc на первом story в MAME переводят прямо в `FIRST_LEVEL`, минуя demo; полная cold-boot приёмка fade остаётся в FG5 | основной титул/Presents/название/Mechner идут в точном порядке `show_title()`; Enter/Space/Esc/стрелки прерывают ожидание; story/intro продолжит FG8 |
|
||
| **FG7** | ✓ | level 0, исходная таблица `demo_moves`, demo HP=4 и блокировка игрового UI реализованы; исправлены зеркалирование auto-control, боевой AI Кида и завершение после смерти; в MAME demo проходит разворот/зацеп, доходит до боя и возвращается в attract-цикл без повторного убийства | `res2000.bin`, исходная `demo_moves`, demo HP=4; бесконечный attract loop, любой ввод начинает чистую новую игру; Pause/QuickSave/читы/таймер отключены |
|
||
| **FG8** | ~ | data-driven interpreter и покадровый PV intro работают; в MAME проверены актёры, факелы, звёзды 1x1, часы/песок, palette-0 lightning и foreground-колонна; Enter/Esc переводят прямо в `FIRST_LEVEL`; временный темп 12,5 FPS и TODO точного pacing записаны в `impl_diff.md` | story/PV intro проходит, любой raw-ввод пропускает его без удержания EMM-страниц |
|
||
| **FG9** | ~ | `pop_flow` корректно маршрутизирует 2/4/6/8/9/12 и ветку <=5 минут (`t_flow`); 2/4/6 и длинная 12 уже обновляют часы, песок, факелы и звёзды каждые 5 кадров Sprinter; длительности всех веток сверены с SDLPoP: 2/4/6/12 — 2,6 с, 8 — 6,0 с, 9 — 7,2 с; входная клавиша gameplay/Shift+L поглощается до сцены, а новое нажатие делает skip; анимации мыши/Princess в 8/9 и разворот Princess в короткой 12 ещё статичны | таблица flow переводит в CUTSCENE ровно перед 2/4/6/8/9/12; scene 12 выбирает короткий вариант при <=5 минутах |
|
||
| **FG10** | ~ | переход TIME_EXPIRED и экран существуют, но это ещё статическая PV-стадия; сквозной MAME-маршрут не принят | PV-сцена истечения с пропуском, затем возврат на title/attract; новая игра сбрасывает таймер |
|
||
| **FG11** | ~ | room 5 уровня 14 переводит в ENDING (`t_flow`); объятие/мышь заменены статической стадией, полный маршрут не принят | room 5 уровня 14 переводит в ENDING; PV-финал и Hail-экран возвращают управление оболочке |
|
||
| **FG12** | ~ | версионированный `POP.HOF`, ввод имени и восстановление после повреждённого файла реализованы; нужна сквозная MAME-проверка ending → HOF → title | версионированный `POP.HOF`, ввод имени raw-клавиатурой, повреждённый файл = пустая таблица, затем title/attract |
|
||
|
||
## 15. Проверки
|
||
|
||
- Host-тест автомата: все допустимые переходы и отсутствие уровня 15.
|
||
- Host-тест cutscene interpreter на синтетическом скрипте и skip в каждой
|
||
ожидающей команде.
|
||
- Host-тест demo input: одинаковый seed даёт одинаковый поток управления.
|
||
- MAME: cold boot -> build info -> title -> demo -> title.
|
||
- MAME: новая игра -> принудительный переход по всем pre-level scenes.
|
||
- MAME: time expired и пропуск сцены.
|
||
- MAME: 13 -> 14 -> room 5 -> ending -> HOF -> title.
|
||
- Проверка EMM/FD до и после каждого состояния: число страниц и открытых
|
||
файлов возвращается к базовому.
|
||
- `make size-check`; крупный cold-код размещать в банках и отдельно следить
|
||
за лимитом 16 КБ каждого банка.
|
||
|
||
## 16. Не входит в план
|
||
|
||
- уровень 15 и copy protection;
|
||
- моды и выбор levelset;
|
||
- replay/recording;
|
||
- точная эмуляция SDL video/controller options;
|
||
- профиль ENHANCED и индивидуальные switches fixes.
|