Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18d177407b | |||
| 0cd6b2d737 | |||
| c6cadd0140 | |||
| 3078886306 | |||
| 4424ea20a8 | |||
| eecb00f911 | |||
| 2b17408609 | |||
| dc8b2b7115 | |||
| 4737ec323c | |||
| cfc3602375 | |||
| 3d8b81c9f6 | |||
| 030af74631 | |||
| 3fe083331f | |||
| 29d60665e0 | |||
| 969f3f1b9a | |||
| 3164065243 | |||
| e67117219f | |||
| 5353bdaaec | |||
| 6aefee0cc7 | |||
| e36828ae6e | |||
| 7ef007757b | |||
| f63751edce | |||
| a89b8b30c6 | |||
| 61d4255091 | |||
| b5d2a81ee3 | |||
| 1f16e8fa70 | |||
| ba37bd1133 | |||
| e86f254b87 | |||
| 1146c57544 | |||
| d552cbaca9 | |||
| 6b4a3b6b41 | |||
| 4b498d171b | |||
| b56f2b4582 | |||
| 774b1cc7c4 | |||
| 2f3e854854 | |||
| 75a51fb1db | |||
| a32b66700f | |||
| 59e51f7e83 | |||
| b22cee3456 | |||
| 2e90eaf7d7 | |||
| 3983fa4513 | |||
| d014a3f577 | |||
| 4f7d9c0596 | |||
| 37fc572cc3 | |||
| 99b430f2ed | |||
| d438a1d3da | |||
| 79ea473910 | |||
| cf06896dbd | |||
| 8bbc6b4d07 | |||
| 565ba98852 | |||
| ac9871c58c | |||
| 1e6c377edc | |||
| 0280b05933 | |||
| f2093e0d89 | |||
| af5f0a4638 | |||
| 3dbad6120c | |||
| 1dc89b0f26 | |||
| e1ee447b7f | |||
| 18f5115e0d | |||
| b3bf2cca3d | |||
| 3b2dd8bfcc | |||
| ecf5ecfc14 | |||
| 4cffbc9aa4 | |||
| 24bb724c22 | |||
| eef6eebd8c | |||
| 3de8c7500c | |||
| 8e33cd07bc | |||
| a5773ab654 | |||
| 68b8fe5851 | |||
| 64ce6339eb |
@@ -113,3 +113,10 @@ mame/
|
|||||||
|
|
||||||
# Claude Code local settings (per-machine, not for the repo)
|
# Claude Code local settings (per-machine, not for the repo)
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# Тяжёлые справочные материалы, НЕ версионируются: docs/extra — архивы
|
||||||
|
# исходников (~570 МБ, четыре почти одинаковых bad_apple), docs/sources —
|
||||||
|
# клоны чужих репозиториев (Sprinter-BIOS, Estex-DSS, SaymanNsk) со своими
|
||||||
|
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
|
||||||
|
docs/extra/
|
||||||
|
docs/sources/
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
# Sprinter C Compiler — top-level Makefile
|
# Sprinter C Compiler — top-level Makefile
|
||||||
#
|
#
|
||||||
# make build host tools, libc archive, all tests, all apps
|
# make build host tools, libc archive, all tests
|
||||||
# make tools build only host tools (mkexe)
|
# make tools build only host tools (mkexe)
|
||||||
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
|
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
|
||||||
# make tests build all libc feature tests under tests/
|
# make tests build all libc feature tests under tests/
|
||||||
# make examples build all real applications under examples/
|
# make examples build all real applications under examples/ (НЕ входит
|
||||||
|
# в `make all`: это регрессная сборка, а examples/ —
|
||||||
|
# крупные приложения, которые её только замедляют
|
||||||
|
# (mdview компилируется минутами) и ничего нового про
|
||||||
|
# libc не показывают. Собирать явно перед `make floppy`.)
|
||||||
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
|
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
|
||||||
# make check run mkexe unit tests
|
# make check run mkexe unit tests
|
||||||
# make clean remove all build artefacts
|
# make clean remove all build artefacts
|
||||||
@@ -39,9 +43,9 @@ DATA_FILES := \
|
|||||||
examples/mdview/SAMPLE.MD
|
examples/mdview/SAMPLE.MD
|
||||||
|
|
||||||
.PHONY: all tools lib tests examples check clean sdcc floppy \
|
.PHONY: all tools lib tests examples check clean sdcc floppy \
|
||||||
size-check size-baseline $(TESTS) $(APPS)
|
size-check size-baseline host-tests $(TESTS) $(APPS)
|
||||||
|
|
||||||
all: tools lib tests examples
|
all: tools lib tests
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
$(MAKE) -C toolchain/mkexe
|
$(MAKE) -C toolchain/mkexe
|
||||||
@@ -73,6 +77,14 @@ floppy: tests examples tests/seek/big.txt
|
|||||||
@echo "Floppy ready: $(FLOPPY_IMG)"
|
@echo "Floppy ready: $(FLOPPY_IMG)"
|
||||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||||
|
|
||||||
|
# Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат
|
||||||
|
# рядом с кодом, который проверяют. MAME не нужна, идут за секунды;
|
||||||
|
# ucsim идёт в комплекте нашего SDCC.
|
||||||
|
HOST_TEST_DIRS := testkit applications/PoP/roomtest/tests-host
|
||||||
|
|
||||||
|
host-tests:
|
||||||
|
@for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done
|
||||||
|
|
||||||
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
|
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
|
||||||
size-check:
|
size-check:
|
||||||
python3 toolchain/size_check.py
|
python3 toolchain/size_check.py
|
||||||
|
|||||||
@@ -59,11 +59,15 @@ Princed для DAT v1.0 (контейнер/индекс/чек-сумма, ко
|
|||||||
|
|
||||||
## Структура папки
|
## Структура папки
|
||||||
|
|
||||||
- `docs/` — планы и форматы: `PORT_PLAN.md` (общий план фаз),
|
- `docs/` — планы и форматы; **индекс с отметками актуальности —
|
||||||
`KID_PLAN.md`, `double_buffer_plan.md`, `loose_floors_plan.md`, форматы
|
`docs/README.md`**, начинать чтение оттуда. Ключевое:
|
||||||
ресурсов. Начинать чтение отсюда.
|
`levels_plan.md` (следующий этап), `layout_plan_v2.md` (раскладка кода по
|
||||||
- `roomtest/` — **активная разработка**: комната 1 + Kid (анимация, ввод,
|
окнам/банкам + скорость отрисовки), `PORT_PLAN.md` (карта фаз со
|
||||||
коллизия, падение, зацеп, fore-окклюзия, loose-полы). Свой `CLAUDE.md`.
|
статусами).
|
||||||
|
- `roomtest/` — **активная разработка**: уровень 1 целиком (Kid, стражи,
|
||||||
|
ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи —
|
||||||
|
`roomtest/TASKS.md`, открытые баги — `roomtest/bug_list.md`, закрытые с
|
||||||
|
разбором корней — `roomtest/bug_closed.md`.
|
||||||
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
||||||
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
||||||
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
||||||
|
|||||||
@@ -4,15 +4,22 @@
|
|||||||
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
||||||
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
||||||
|
|
||||||
Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
**Состояние (2026-08-01): играется весь уровень 1** — комнаты и переходы,
|
||||||
Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
Kid со всем набором действий, ловушки, ворота, дверь уровня, меч и бой,
|
||||||
|
стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука,
|
||||||
|
таймера/HUD, сохранений.
|
||||||
|
|
||||||
|
- Что в работе прямо сейчас — [`roomtest/TASKS.md`](roomtest/TASKS.md).
|
||||||
|
- Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md).
|
||||||
|
- Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
||||||
|
- Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
||||||
|
|
||||||
## Что где
|
## Что где
|
||||||
|
|
||||||
| Папка | Назначение |
|
| Папка | Назначение |
|
||||||
|-------|-----------|
|
|-------|-----------|
|
||||||
| `roomtest/` | **Активная разработка.** Комната 1 уровня 1 живой композицией тайлов + Kid: анимация (seqtbl), управление с клавиатуры, коллизия, падение, зацеп/подтягивание, fore-окклюзия, проваливающиеся полы. Свой README/CLAUDE. |
|
| `roomtest/` | **Активная разработка.** Уровень 1 целиком: фон композицией тайлов, Kid (seqtbl-анимация, ввод, коллизия, падение, зацеп, окклюзия), ловушки, ворота, стражи, бой. Свой README/CLAUDE/TASKS. |
|
||||||
| `docs/` | Планы (`PORT_PLAN`, `KID_PLAN`, `double_buffer_plan`, `loose_floors_plan`) и разбор форматов ресурсов Apple II / DOS. |
|
| `docs/` | Планы и разбор форматов ресурсов Apple II / DOS — см. индекс в [`docs/README.md`](docs/README.md). |
|
||||||
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
||||||
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
||||||
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
# Prince of Persia — Kid (персонаж): анализ и план
|
# Prince of Persia — Kid (персонаж): анализ и план
|
||||||
|
|
||||||
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
|
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Kid играется целиком: интерпретатор
|
||||||
|
> `seqtbl` + `frame_table` (`roomtest/pop_kid.c`), диспетчер `control()`
|
||||||
|
> (`pop_ctrl.c`), коллизия/физика/зацеп (`pop_map.c`), бой и HP. Модель
|
||||||
|
> персонажа стала общей: `Char`-окно (`pop_state.c`) обслуживает и Кида, и
|
||||||
|
> стражей. Таблицы кадров и `seqtbl` уехали из `_CODE` в EMM-страницу
|
||||||
|
> (`kid_data.bin`, см. `layout_plan_v2.md` шаг 1).
|
||||||
|
>
|
||||||
|
> Отступление от §2.1 плана: выбран ПАДДИНГ кадров (общий канвас), а не
|
||||||
|
> per-frame offset — компромисс зафиксирован в `PORT_PLAN.md §6.1`.
|
||||||
|
>
|
||||||
|
> **Документ оставлен как СПРАВОЧНИК по модели персонажа** (`char_type`,
|
||||||
|
> категории `actions_*`, устройство `play_seq`, объём спрайтов) — он нужен
|
||||||
|
> при портировании остальных акторов (скелет, тень, визирь). Текущие
|
||||||
|
> задачи — `../roomtest/TASKS.md`.
|
||||||
|
|
||||||
|
Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c`
|
||||||
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
|
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
|
||||||
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
|
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
|
||||||
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
|
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
# Prince of Persia на ZX Sprinter — план порта
|
# Prince of Persia на ZX Sprinter — план порта
|
||||||
|
|
||||||
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
|
## СТАТУС (обновлено 2026-08-01)
|
||||||
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
|
|
||||||
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
|
Документ составлен 2026-07-15 как план «с нуля» и с тех пор во многом
|
||||||
|
исполнен. Читать его надо так:
|
||||||
|
|
||||||
|
| Раздел | Что с ним сейчас |
|
||||||
|
|--------|------------------|
|
||||||
|
| §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) |
|
||||||
|
| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS.md` (KBD-1) |
|
||||||
|
| §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) |
|
||||||
|
| §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) |
|
||||||
|
| §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой |
|
||||||
|
| §6 модель движения | **сделано**: `play_seq` + `frame_table` оригинала, не физика с нуля |
|
||||||
|
| §7 фазы | см. отметки статуса прямо в разделе |
|
||||||
|
| §8 риски | п.1 закрыт, п.3 закрыт (28 страниц-атласов Кида), п.2/п.4 — см. отметки в разделе |
|
||||||
|
| §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` |
|
||||||
|
|
||||||
|
**Где смотреть текущее состояние, а не план:** `../roomtest/README.md`
|
||||||
|
(что играется), `../roomtest/TASKS.md` (что в работе), `levels_plan.md`
|
||||||
|
(следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Опирается на
|
||||||
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
|
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
|
||||||
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
|
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
|
||||||
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
|
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
|
||||||
@@ -227,6 +248,13 @@ dirty-биты, heal против фона через ОЗУ-копию); `room_
|
|||||||
|
|
||||||
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
|
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
|
||||||
|
|
||||||
|
> **Закрыт (исторический раздел).** PoC в `poc/` свою задачу выполнил и
|
||||||
|
> дальше не развивается: управление ощущается как PoP, held-state работает.
|
||||||
|
> Всё, что ниже про плейсхолдер-персонажа и приблизительную дугу прыжка,
|
||||||
|
> — уже неправда для активной ветки: в `roomtest/` стоит настоящая графика
|
||||||
|
> Кида и авторские таблицы кадров (§6). Раздел оставлен ради истории
|
||||||
|
> решений (в частности §5.1 — почему сначала был плейсхолдер).
|
||||||
|
|
||||||
**Объём**: одна комната (например Level 1, экран старта Кида), без
|
**Объём**: одна комната (например Level 1, экран старта Кида), без
|
||||||
переходов между экранами, без стражников (стретч-цель, не обязательна).
|
переходов между экранами, без стражников (стретч-цель, не обязательна).
|
||||||
|
|
||||||
@@ -364,45 +392,53 @@ memory/png_strip_padding_tradeoff.
|
|||||||
## 7. Полноценное приложение — фазы (после PoC)
|
## 7. Полноценное приложение — фазы (после PoC)
|
||||||
|
|
||||||
Порядок — по риску и зависимостям, не по геймплейной важности.
|
Порядок — по риску и зависимостям, не по геймплейной важности.
|
||||||
|
**Отметки статуса — на 2026-08-01.**
|
||||||
|
|
||||||
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
|
**Фаза 0 — инфраструктура порта** — **СДЕЛАНА**, но иначе, чем задумано:
|
||||||
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
|
- Хелд-стейт клавиатуры по §2 — сделан.
|
||||||
подтверждения пользователем).
|
- Конвертер уровней не понадобился: `res200N.bin` из `SDLPoP/data/LEVELS`
|
||||||
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
|
кладётся на образ как есть и читается по офсетам в рантайме
|
||||||
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
|
(`roomtest/pop_level.c`), уровень живёт в EMM-странице.
|
||||||
не обязательно разворачивать в C-struct с указателями).
|
- Конвертер фона в растры **отменён осознанно** (§4): фон собирается
|
||||||
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
|
тайлами в рантайме. Спрайты — `toolchain/pop_pack_bg.py` /
|
||||||
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
|
`pop_pack_kid.py` / `pop_pack_guard.py` → атласы `.atl` (Kid — 28
|
||||||
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
|
страниц, риск §8 п.3 закрыт).
|
||||||
хватит текущего лимита — см. риск в §8).
|
|
||||||
|
|
||||||
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
|
**Фаза 1 — Кид, полный набор действий** — **СДЕЛАНА**: стоять/идти/бежать/
|
||||||
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
|
тормозить/разворот/прыжки/повисание/подтягивание/спуск/приседание/
|
||||||
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
|
осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
|
||||||
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
|
комнатами во все четыре стороны. Осталось: **старт по данным уровня**
|
||||||
|
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
|
||||||
|
`../roomtest/TASKS.md`.
|
||||||
|
|
||||||
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
|
**Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
|
||||||
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
|
`LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
|
||||||
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
|
потолка), зелья, дверь уровня (открывается), факелы. Подробности и
|
||||||
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
|
справочник — `gates_spikes_plan.md`.
|
||||||
|
|
||||||
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
|
**Фаза 3 — бой** — **СДЕЛАНА в объёме обычного стражника**: подбор и
|
||||||
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
|
выхватывание меча, стойка, удар/парирование, коллизия клинков, HP обеих
|
||||||
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
|
сторон, смерть; ИИ стража (замечает Кида, подходит, боевые ветки),
|
||||||
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
|
персистентность трупа между комнатами.
|
||||||
спереди/сзади».
|
|
||||||
|
|
||||||
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
|
**Фаза 4 — разнообразие противников** — **НЕ НАЧАТА**. Скелет нужен на
|
||||||
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
|
уровне 3, толстый — на 6, тень — на 12, визирь — на 13; привязка
|
||||||
§3), толстый стражник/визирь (общая база анимации с визирем).
|
«уровень → тип стража» (`tbl_guard_type`) описана в `levels_plan.md` §1.
|
||||||
|
|
||||||
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
|
**Фаза 5 — звук** — **НЕ НАЧАТА**: CBL-эффекты (шаги, удары, двери,
|
||||||
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
|
падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как
|
||||||
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
|
опциональный дешёвый бипер без CBL, если формат подтвердится простым
|
||||||
|
парсингом. Опкод `SOUND` в `play_seq` пока просто съедает свой аргумент —
|
||||||
|
точки вызова уже на месте.
|
||||||
|
|
||||||
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
|
**Фаза 6 — оболочка** — **НЕ НАЧАТА**: титры, меню/выбор уровня, HUD
|
||||||
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
|
(таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по
|
||||||
геймплейно не критично.
|
минимуму, геймплейно не критично. Полоса HP — единственное, что уже есть.
|
||||||
|
|
||||||
|
**Между Фазами 4 и 5 вклинивается то, чего в этом плане не было:
|
||||||
|
переход между УРОВНЯМИ** (загрузка следующего уровня, второй тайлсет
|
||||||
|
palace, потабличные различия уровней). Отдельный документ —
|
||||||
|
`levels_plan.md`.
|
||||||
|
|
||||||
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
|
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
|
||||||
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
|
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
|
||||||
@@ -418,23 +454,33 @@ tiny/small.
|
|||||||
|
|
||||||
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
|
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
|
||||||
|
|
||||||
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
|
1. ~~**Held-state клавиатуры** (§2)~~ — **закрыт** (`<kbd_raw.h>`). Открытый
|
||||||
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
|
остаток — не «есть ли held-state», а потеря байт при аккордах
|
||||||
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
|
Shift+стрелка: `../roomtest/TASKS.md`, KBD-1.
|
||||||
несколько анимированных ловушек может приблизиться к лимиту
|
2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
|
||||||
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
|
спрайтовый движок для персонажей не используется, поэтому лимит
|
||||||
уровням (сколько объектов одновременно активно в худшем экране).
|
«~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
|
||||||
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
|
перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
|
||||||
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
|
(~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()`
|
||||||
атласов на актора с переключением по фазе действия (стоять/идти отдельно
|
в `roomtest.c`. План выжимания — `../roomtest/TASKS.md` (CLIP-1) и
|
||||||
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
|
`../roomtest/bug_list.md` (T-1/T-2).
|
||||||
атласы — оценить фактический байтовый вес конвертированных кадров Кида
|
3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
|
||||||
прежде чем проектировать.
|
атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
|
||||||
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
|
мульти-страничного формата `.atl` не потребовалось. Побочно
|
||||||
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
|
подтвердился компромисс паддинга (§6.1).
|
||||||
Sprinter; если оригинал считался на другой частоте — потребуется
|
4. **Тайминг оригинала** — **ОТКРЫТ, и сверка 2026-08-01 показывает
|
||||||
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
|
расхождение.** Цифры оригинала (SDLPoP): базовый таймер `BASE_FPS = 60`
|
||||||
визуально быстрее/медленнее эталона.
|
(`types.h:1373`), логический кадр игры — `base_speed = 5` тиков
|
||||||
|
(`data.h:869`), то есть **83.3 мс (12 лог. кадров/с)**; в бою
|
||||||
|
`fight_speed = 6` → **100 мс (10/с)**. У нас (`roomtest.c`) — три
|
||||||
|
ожидания `gfx_wait_vsync()` на итерацию, то есть **60 мс (16.7/с)** и
|
||||||
|
без отдельной скорости боя. Значит **игра идёт примерно на 39 %
|
||||||
|
быстрее эталона**. Точное соответствие даёт 4 ожидания vsync (80 мс
|
||||||
|
против 83.3) и 5 в бою (100 мс — совпадает точно).
|
||||||
|
Проверять не «на глаз», а секундомером по одинаковому отрезку
|
||||||
|
(SDLPoP рядом на том же экране), и только после того, как кадр
|
||||||
|
перестанет иногда вылезать за период (см. п.2) — иначе замедление
|
||||||
|
спрячет проблему бюджета вместо того, чтобы её показать.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,40 @@
|
|||||||
# Форматы ресурсов Prince of Persia — сводка
|
# `applications/PoP/docs` — индекс + сводка по форматам ресурсов
|
||||||
|
|
||||||
|
## Индекс документов (актуальность на 2026-08-01)
|
||||||
|
|
||||||
|
**Живые планы — читать перед работой:**
|
||||||
|
|
||||||
|
| Документ | О чём |
|
||||||
|
|----------|-------|
|
||||||
|
| [`../roomtest/TASKS.md`](../roomtest/TASKS.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) |
|
||||||
|
| [`../roomtest/bug_list.md`](../roomtest/bug_list.md) | Открытые баги roomtest (закрытые — в `bug_closed.md` рядом) |
|
||||||
|
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
||||||
|
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
|
||||||
|
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди |
|
||||||
|
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии из `bug_closed.md`, дифф против SDLPoP |
|
||||||
|
| [`ideas_backlog.md`](ideas_backlog.md) | Осознанно отложенные гипотезы (мышь, PRNG) |
|
||||||
|
| [`prng_alternatives.md`](prng_alternatives.md) | Запасные генераторы, если упрёмся в бюджет кадра |
|
||||||
|
|
||||||
|
**Исполненные планы, оставленные как справочники:**
|
||||||
|
|
||||||
|
| Документ | Чем ещё полезен |
|
||||||
|
|----------|-----------------|
|
||||||
|
| [`PORT_PLAN.md`](PORT_PLAN.md) | Общая карта фаз со статусами; §6 (модель движения), §10 (режим памяти) |
|
||||||
|
| [`KID_PLAN.md`](KID_PLAN.md) | Модель персонажа: `char_type`, `actions_*`, устройство `play_seq` — нужна для скелета/тени/визиря |
|
||||||
|
| [`gates_spikes_plan.md`](gates_spikes_plan.md) | Раскладка объектов уровня 1 по комнатам, декод `LINKLOC`/`LINKMAP`, точные ссылки на seg-код |
|
||||||
|
|
||||||
|
**Форматы ресурсов** (ниже по этому файлу): `POP-DAT-FormatSpecifications.pdf`
|
||||||
|
/ `.txt` (первоисточник), `APPLEII_RESOURCE_FORMAT.md`,
|
||||||
|
`MSDOS_RESOURCE_FORMAT.md`.
|
||||||
|
|
||||||
|
Удалены 2026-08-01 как полностью исполненные и перекрытые кодом:
|
||||||
|
`clip_char_plan.md`, `double_buffer_plan.md`, `loose_floors_plan.md`,
|
||||||
|
`size_optimization_plan.md` (его §8 про скорость отрисовки перенесён в
|
||||||
|
`layout_plan_v2.md` §9). Ищутся в истории git, если понадобятся.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Форматы ресурсов — сводка
|
||||||
|
|
||||||
**Каноническая спецификация форматов** — `POP-DAT-FormatSpecifications.pdf`
|
**Каноническая спецификация форматов** — `POP-DAT-FormatSpecifications.pdf`
|
||||||
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
|
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
|
||||||
@@ -70,7 +106,11 @@ DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`B
|
|||||||
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
|
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
|
||||||
декодер сжатия пикселей DOS `.DAT`.
|
декодер сжатия пикселей DOS `.DAT`.
|
||||||
|
|
||||||
## Что дальше (не сделано в этом заходе)
|
## Что дальше по форматам (не сделано и пока не нужно)
|
||||||
|
|
||||||
|
Порт читает уровень напрямую из `res200N.bin` (`roomtest/pop_level.c`), а
|
||||||
|
графику берёт из распакованных PNG `SDLPoP/data/` — поэтому ни один пункт
|
||||||
|
ниже сейчас не блокирует работу.
|
||||||
|
|
||||||
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
|
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
|
||||||
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
|
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
# План: порт `clip_char()` — обрезка спрайта персонажа
|
|
||||||
|
|
||||||
Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8»
|
|
||||||
(Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке).
|
|
||||||
|
|
||||||
## Симптом
|
|
||||||
|
|
||||||
room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт
|
|
||||||
Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии
|
|
||||||
пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке.
|
|
||||||
|
|
||||||
## Корень
|
|
||||||
|
|
||||||
Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из
|
|
||||||
`add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ
|
|
||||||
`set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД
|
|
||||||
`add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и
|
|
||||||
прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только
|
|
||||||
внутри него. Мы рисуем без клипа вообще.
|
|
||||||
|
|
||||||
## Что делает оригинал (дословно)
|
|
||||||
|
|
||||||
`reset_obj_clip()` → `left=0, top=0, right=320, bottom=192`.
|
|
||||||
|
|
||||||
Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый
|
|
||||||
`get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена
|
|
||||||
ИЛИ пол»):
|
|
||||||
|
|
||||||
```
|
|
||||||
T_L = get_tile(room, char_col_left, char_top_row)
|
|
||||||
T_R = get_tile(room, char_col_right, char_top_row)
|
|
||||||
if (T_L — стена или пол) &&
|
|
||||||
( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */
|
|
||||||
|| (T_R — стена или пол) )
|
|
||||||
{
|
|
||||||
clip_row = Char.curr_row + 1;
|
|
||||||
clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */
|
|
||||||
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y))
|
|
||||||
obj_clip_top = char_top_y = clip_y;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой
|
|
||||||
пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»).
|
|
||||||
Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО.
|
|
||||||
|
|
||||||
Метрики из `set_char_collision` (seg006:0723):
|
|
||||||
- `char_x_left = obj_x/2 + 58` (и `-= char_width_half`, если смотрит вправо)
|
|
||||||
- `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2`
|
|
||||||
- `char_top_y = obj_y - h + 1`; если `>= 192` → `0`
|
|
||||||
- `char_top_row = y_to_row_mod4(char_top_y)`
|
|
||||||
- `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`,
|
|
||||||
`char_col_right = MIN(get_tile_div_mod(char_x_right), 9)`
|
|
||||||
|
|
||||||
Второй блок `clip_char` — `obj_clip_right` (doortop / стена / зеркало для
|
|
||||||
виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228
|
|
||||||
(выход в дверь уровня). **Фаза 2**, не в этом заходе.
|
|
||||||
|
|
||||||
## Наши точки касания
|
|
||||||
|
|
||||||
- `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит
|
|
||||||
через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает
|
|
||||||
прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`.
|
|
||||||
- `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`,
|
|
||||||
`get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже
|
|
||||||
есть в `check_spike_below()` (строка ~1198) — брать за образец.
|
|
||||||
- `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0`
|
|
||||||
пропускает `sy` верхних строк колонки), но произвольной верхней границы нет.
|
|
||||||
|
|
||||||
## Шаги
|
|
||||||
|
|
||||||
1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
|
||||||
int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`».
|
|
||||||
Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник
|
|
||||||
`+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`.
|
|
||||||
Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро).
|
|
||||||
2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w,
|
|
||||||
uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y
|
|
||||||
(0 = клипа нет). Экспорт в `pop_map.h`.
|
|
||||||
3. **pop_kid.c**: в `kid_draw()` после расчёта `top` —
|
|
||||||
`ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`,
|
|
||||||
рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`;
|
|
||||||
в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее
|
|
||||||
и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает
|
|
||||||
`reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять).
|
|
||||||
4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`,
|
|
||||||
`make size-check`; вывести свободное место в W2/W3 (порог 512 Б).
|
|
||||||
5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после
|
|
||||||
пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск.
|
|
||||||
Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`).
|
|
||||||
|
|
||||||
## Риски / что проверить отдельно
|
|
||||||
|
|
||||||
- `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт −1 для
|
|
||||||
полосы у потолка; `get_tile(row −1)` уже умеет ряд 2 верхнего соседа.
|
|
||||||
- Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип
|
|
||||||
прыгнет.
|
|
||||||
- `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в
|
|
||||||
обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой
|
|
||||||
должно отсекать).
|
|
||||||
- Клип меняет прямоугольник heal → возможен «хвост» на второй странице
|
|
||||||
дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер).
|
|
||||||
|
|
||||||
## Дальше (Фаза 2, отдельно)
|
|
||||||
|
|
||||||
`obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при
|
|
||||||
взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по
|
|
||||||
колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но
|
|
||||||
без тестовой сцены проверять нечем.
|
|
||||||
|
|
||||||
См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`,
|
|
||||||
`pop_check_sdlpop_first`, `gfx_blit_noclip_fast`.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Double buffer (два экрана + флип) — план
|
|
||||||
|
|
||||||
Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей)
|
|
||||||
и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком
|
|
||||||
слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не
|
|
||||||
промежуточные состояния heal-рендера). **Тумблер обязателен** —
|
|
||||||
однобуферный режим удобнее для отладки багов рисования.
|
|
||||||
|
|
||||||
## Что уже готово (libbgi — трогать НЕ нужно)
|
|
||||||
|
|
||||||
- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает
|
|
||||||
`_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы),
|
|
||||||
`gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS).
|
|
||||||
- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов.
|
|
||||||
- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync();
|
|
||||||
set_visible_page(hidden);`
|
|
||||||
- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры
|
|
||||||
(page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ.
|
|
||||||
|
|
||||||
## Текущая модель рендера roomtest (однобуфер)
|
|
||||||
|
|
||||||
`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты
|
|
||||||
рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT).
|
|
||||||
Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()`
|
|
||||||
(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick`
|
|
||||||
→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее
|
|
||||||
бланка, луч ловит промежуток.
|
|
||||||
|
|
||||||
## Работа на стороне PoP
|
|
||||||
|
|
||||||
1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1
|
|
||||||
(теневая копия одна — общая, из неё heal'ит любая страница).
|
|
||||||
2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync.
|
|
||||||
Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync —
|
|
||||||
plane-параметр).
|
|
||||||
3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в
|
|
||||||
массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)`
|
|
||||||
восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем
|
|
||||||
через страницу). То же для fore/пол-оверлея, если они рисуют вне
|
|
||||||
футпринта Кида.
|
|
||||||
4. **Ping-pong в цикле**:
|
|
||||||
```
|
|
||||||
uint8_t back = dbuf ? (front ^ 1) : 0;
|
|
||||||
gfx_set_draw_page(back);
|
|
||||||
kid_heal(back); kid_tick; phys; kid_draw; fore;
|
|
||||||
gfx_wait_vsync();
|
|
||||||
if (dbuf) { gfx_set_visible_page(back); front = back; }
|
|
||||||
```
|
|
||||||
5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] —
|
|
||||||
бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или
|
|
||||||
compile-флагом.
|
|
||||||
6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед
|
|
||||||
свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным
|
|
||||||
счётом кадров, чтобы скорость игры не изменилась.
|
|
||||||
|
|
||||||
## Порядок
|
|
||||||
|
|
||||||
- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page
|
|
||||||
heal (проверить, что флип работает, фон корректен на обеих).
|
|
||||||
- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида.
|
|
||||||
- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет).
|
|
||||||
- D4: пейсинг (fps_div) — вернуть исходную скорость.
|
|
||||||
|
|
||||||
## Связанные
|
|
||||||
|
|
||||||
Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]];
|
|
||||||
heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors
|
|
||||||
[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в
|
|
||||||
обе страницы по той же дисциплине.
|
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
|
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
|
||||||
|
|
||||||
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ
|
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Все фазы плана (P0 персистентный
|
||||||
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст).
|
> per-room `room_modif`, S пики, B кнопки+ворота) сделаны и играются:
|
||||||
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
> `roomtest/pop_trob.c` (trob-диспетчер, `LINKLOC`/`LINKMAP`, ворота, дверь
|
||||||
|
> уровня, факелы, зелья), `pop_map.c` (HP, смерть на пиках, урон падения),
|
||||||
|
> `pop_redraw.c` (пометки перерисовки вместо прямых блитов). Ограничения
|
||||||
|
> из §0 закрыты: тайлы персистентны, HP/смерть есть, loose обобщён в trob;
|
||||||
|
> L3-вверх (climb-up в комнату сверху) тоже сделан (`pop_leave_dir = 3`).
|
||||||
|
> Из §5 остаётся открытым только **переход на следующий уровень через дверь
|
||||||
|
> уровня** — он вынесен в `levels_plan.md`.
|
||||||
|
>
|
||||||
|
> **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка
|
||||||
|
> объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные
|
||||||
|
> ссылки на механику SDLPoP) продолжают экономить время при отладке.
|
||||||
|
> Текущие задачи — `../roomtest/TASKS.md`.
|
||||||
|
|
||||||
|
Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт
|
||||||
|
«с чистого листа» (пустой контекст). Всё сверено с
|
||||||
|
`applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
||||||
|
|
||||||
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
||||||
перед кодингом читать соответствующий код seg*.c, не гадать.
|
перед кодингом читать соответствующий код seg*.c, не гадать.
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# План: модульные тесты движка roomtest под ucsim_z80
|
||||||
|
|
||||||
|
Обвязка общая — `testkit/` в корне репозитория (там же объяснение, почему
|
||||||
|
прогон именно под z80, а не хостовым gcc). Наборы лежат в
|
||||||
|
`../roomtest/tests-host/`.
|
||||||
|
|
||||||
|
Задача плана: **перестать чинить одно и то же дважды**. За два прогона
|
||||||
|
уровня 1 (2026-08-03) закрыто восемь корней, и часть из них — регрессии
|
||||||
|
соседней механики, внесённые предыдущим фиксом. Такие вещи ловятся тестом
|
||||||
|
за миллисекунды, а в MAME — часами ручного вождения Кида.
|
||||||
|
|
||||||
|
## Что уже есть
|
||||||
|
|
||||||
|
| набор | модуль | статус |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| `t_geom` | `pop_geom.c` | 39 проверок, включая побитовую сверку asm-LCG с 32-битной формулой на 128 шагах |
|
||||||
|
|
||||||
|
`pop_geom.c` выбран первым, потому что не тянет ничего за собой. Дальше
|
||||||
|
начинаются швы.
|
||||||
|
|
||||||
|
## Фаза 1. Два шва (блокирует всё остальное)
|
||||||
|
|
||||||
|
### 1.1 Доступ к странице уровня
|
||||||
|
|
||||||
|
`pop_level.c` ходит по абсолютным адресам: `gfx_w0_map(lvl_page)`, затем
|
||||||
|
разыменование `(uint8_t *)(LVL_DATA_OFF + …)`. В тестовом бинаре это
|
||||||
|
обращение в никуда.
|
||||||
|
|
||||||
|
Нужен макрос `W0PTR(off)`:
|
||||||
|
|
||||||
|
- на таргете — `((uint8_t *)(off))`, то есть ровно как сейчас;
|
||||||
|
- в тестах — смещение в обычном массиве-подложке.
|
||||||
|
|
||||||
|
Правка механическая и компайл-таймовая, на размер продукта не влияет.
|
||||||
|
Заодно снимает магию абсолютных констант из тела функций.
|
||||||
|
|
||||||
|
Тестовая подложка должна уметь: загрузить синтетическую комнату (10×3
|
||||||
|
байта fg + mod) и целый синтетический уровень на 24 комнаты, чтобы
|
||||||
|
проверять межкомнатные вещи.
|
||||||
|
|
||||||
|
### 1.2 Журналирующий рендерер
|
||||||
|
|
||||||
|
Вместо `pop_bg.c`/`pop_gdraw.c` в тестовый бинарь линкуется модуль с теми
|
||||||
|
же прототипами, который **не рисует, а записывает вызовы**: какой тайл
|
||||||
|
помечен к перерисовке, каким кодом, с каким счётчиком страниц.
|
||||||
|
|
||||||
|
Это не обход проблемы, а самостоятельная ценность: `BUG-GATE-ANIM-1` был
|
||||||
|
ровно такой формы — ворота меняли состояние, но пометка на перерисовку не
|
||||||
|
ставилась. Проверяется утверждением, а не глазами.
|
||||||
|
|
||||||
|
Минимум, который надо перехватывать: `pop_set_redraw`,
|
||||||
|
`pop_set_redraw_above`, `pop_loose_mob_spawn`, `pop_gate_redraw`.
|
||||||
|
|
||||||
|
## Фаза 2. Регрессионные кейсы из `bug_closed.md`
|
||||||
|
|
||||||
|
После швов `bug_closed.md` превращается в готовую спецификацию: у каждой
|
||||||
|
записи есть симптом и ожидаемое поведение. Кандидаты, которые ловятся
|
||||||
|
логикой (без отрисовки и без железа):
|
||||||
|
|
||||||
|
| баг | что закрепить тестом |
|
||||||
|
|-----|----------------------|
|
||||||
|
| `BUG-LVLSTATE-1` | запись тайла переживает выход из комнаты |
|
||||||
|
| `BUG-RESPAWN-1` | рестарт уровня возвращает ВСЕ тайлы из эталонной копии |
|
||||||
|
| `BUG-RESPAWN-2` | рестарт возвращает таблицу стражей; убитый снова жив |
|
||||||
|
| `BUG-GATE-ANIM-1` | смена состояния ворот ставит пометку `POP_RD_GATE`; закрывающиеся — на обе страницы, открывающиеся — на одну |
|
||||||
|
| `BUG-COLL-1` | `check_collisions` сканирует ряд справа налево и выбирает НАИМЕНЬШУЮ занятую колонку |
|
||||||
|
| `BUG-STANDUP-1` | `bumped_floor` у трупа (`alive >= 0`) только выравнивает и не трогает последовательность |
|
||||||
|
| `BUG-DEATH-1` | `hitp_curr == 0` при живом Киде переводит его в «умирает» ровно один раз |
|
||||||
|
| `BUG-LOOSE-2` | кусок, начавший падать, долетает и кладёт щебень ПОСЛЕ смены комнаты |
|
||||||
|
| `BUG-CEIL-2` | loose-плита ряда 2 верхнего соседа живёт как «ряд −1» |
|
||||||
|
|
||||||
|
`BUG-LOOSE-2` стоит взять первым: он до сих пор помечен в `bug_list.md`
|
||||||
|
как непроверенный именно потому, что гонку «уйти из комнаты раньше, чем
|
||||||
|
долетит плита» через мост MAME воспроизвести не удалось. На уровне логики
|
||||||
|
это несколько строк — заспавнить кусок, сменить комнату, тикать до
|
||||||
|
приземления, проверить щебень в данных уровня.
|
||||||
|
|
||||||
|
Не берутся (нужна картинка либо железо): `BUG-DOOR-CLIP`, `BUG-CEIL-1`,
|
||||||
|
`BUG-CEIL-3`, `BUG-OCCL-1`, `BUG-KBD-4`, `BUG-3`.
|
||||||
|
|
||||||
|
## Фаза 3. Сценарные тесты
|
||||||
|
|
||||||
|
Сейчас шаг кадра размазан по `main()` в `roomtest.c`. Вынести его в
|
||||||
|
`pop_frame_tick()` — тогда появляются тесты вида «поставить Кида в
|
||||||
|
известное состояние, скормить N тиков ввода, проверить итог»:
|
||||||
|
|
||||||
|
```
|
||||||
|
дано: комната 5, Кид на кнопке (0,6)
|
||||||
|
когда: 40 тиков без ввода
|
||||||
|
тогда: комната по-прежнему 5, Кид на полу ряда 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Это тот самый BUG-STANDUP-1, который ловили потиковой трассой в MAME.
|
||||||
|
|
||||||
|
Ввод подаётся не через `kbd_raw_down()`, а через подменяемый источник —
|
||||||
|
это же даст возможность проигрывать записанные сценарии.
|
||||||
|
|
||||||
|
## Фаза 4. Дифф против SDLPoP
|
||||||
|
|
||||||
|
`SDLPoP/src/` лежит в дереве, собирается на хосте, и там **уже стоят
|
||||||
|
отладочные трассы** (`DBG kidobj tilepos=…` в seg008, `DBG make_loose_fall`
|
||||||
|
в seg007). Значит эталон можно заставить печатать потиковую трассу
|
||||||
|
автоматически.
|
||||||
|
|
||||||
|
Схема: общий формат скрипта ввода и общий формат трассы (тик, frame, x, y,
|
||||||
|
room, col, row, action, alive, hp). Гоняем обе реализации, диффим, первое
|
||||||
|
расхождение — номер тика и есть баг. Это ровно то, что делалось руками
|
||||||
|
через MAME, только бесплатно и повторяемо: `BUG-COLL-1` и `BUG-STANDUP-1`
|
||||||
|
такой дифф нашёл бы за секунды.
|
||||||
|
|
||||||
|
**Лицензия.** SDLPoP — GPLv3, правило подпроекта — читать и переписывать,
|
||||||
|
не линковать. Оракул обязан быть **отдельным исполняемым файлом**,
|
||||||
|
общающимся через файлы трасс, а не слинкованным с нашим кодом в один
|
||||||
|
бинарь.
|
||||||
|
|
||||||
|
Требование к детерминизму: сиды PRNG должны совпадать. У нас
|
||||||
|
`POP_PRANDOM_EXACT` даёт ту же последовательность, что в оригинале, и это
|
||||||
|
уже закреплено тестом `geom_lcg_matches_reference`.
|
||||||
|
|
||||||
|
## Чего эти тесты не поймают
|
||||||
|
|
||||||
|
Отрисовку, банки и W-окна, тайминги, клавиатуру — за этим остаётся MAME.
|
||||||
|
|
||||||
|
И отдельный класс: **баги порядка вызовов**. Свежий пример — окно
|
||||||
|
fore-клипа (`pop_fore_set_clip`) одно на всех, и его ставит каждый, кто
|
||||||
|
рисует персонажа; когда порядок «Кид/страж» стал переменным, окно осталось
|
||||||
|
стражьим, и Кид нарисовался поверх передних столбов. Это не «функция
|
||||||
|
вернула не то», unit-тест такое не видит. Ловится инвариантом,
|
||||||
|
вкомпилированным в safe-сборку: «в момент `pop_fore_over_kid` окно клипа
|
||||||
|
принадлежит Киду». Отдельный инструмент, дополняющий тесты.
|
||||||
|
|
||||||
|
## Порядок работ
|
||||||
|
|
||||||
|
1. Шов `W0PTR` + подложка уровня.
|
||||||
|
2. Журналирующий рендерер.
|
||||||
|
3. `BUG-LOOSE-2` — закрыть висящий вопрос.
|
||||||
|
4. Остальные кейсы из таблицы фазы 2.
|
||||||
|
5. `pop_frame_tick()` + сценарные тесты.
|
||||||
|
6. Дифф против SDLPoP.
|
||||||
|
|
||||||
|
Правило приёмки: тест не считается написанным, пока не проверен мутацией —
|
||||||
|
сломать проверяемое место и убедиться, что набор краснеет.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Идеи и вопросы «на подумать» (PoP)
|
||||||
|
|
||||||
|
Не план работ, а список того, что осознанно отложено: каждая запись —
|
||||||
|
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
||||||
|
сделать это прямо сейчас.
|
||||||
|
|
||||||
|
## Зелье «переворот экрана» (upside-down)
|
||||||
|
|
||||||
|
**Вопрос пользователя (2026-08-01).** Тайлы фона у нас лежат строками, а
|
||||||
|
кадры Кида/стражей — КОЛОНКАМИ (`transpose_cols` в `pop_pack_kid.py`, ради
|
||||||
|
бесплатного горизонтального зеркала). Значит вертикальный переворот для
|
||||||
|
персонажей заметно сложнее, чем для фона. Верно; но прежде чем это чинить,
|
||||||
|
надо знать три факта.
|
||||||
|
|
||||||
|
**Факт 1 — когда оно вообще нужно.** Зелье переворота — тип 4
|
||||||
|
(`proc_get_object`, `seg006.c:1885` → `toggle_upside()`). Скан всех уровней
|
||||||
|
по данным (`res200N.bin`, тайл 10 = зелье, тип в backtable): тип 4
|
||||||
|
встречается **впервые на уровне 9** (две склянки), и больше нигде. Тип 3
|
||||||
|
(перо, медленное падение) — уровень 7. То есть **до уровня 9 механика не
|
||||||
|
нужна вообще**, и «на первом этапе просто не реализовывать» — не компромисс,
|
||||||
|
а точное соответствие данным уровней 1..8.
|
||||||
|
|
||||||
|
**Факт 2 — что именно делает оригинал.** НЕ переворачивает спрайты.
|
||||||
|
`flip_screen` (`seg009.c:1042`) → `flip_not_ega` (`seg009.c:1023`) меняет
|
||||||
|
местами СТРОКИ готового offscreen-буфера (top↔bottom, порядок пикселей
|
||||||
|
внутри строки не трогает — это вертикальное зеркало, не поворот на 180°).
|
||||||
|
Вызывается вокруг отрисовки кадра целиком (`seg003.c:296..301`): перевернул
|
||||||
|
буфер → дорисовал → перевернул обратно. Так что в оригинале это
|
||||||
|
post-process всего экрана, и вопрос «как перевернуть колоночный спрайт»
|
||||||
|
там просто не возникает.
|
||||||
|
|
||||||
|
**Факт 3 — почему нам этот приём не подходит как есть.** У нас нет шага
|
||||||
|
«готовый offscreen → экран»: рисуем прямо в видеостраницу, а heal берёт фон
|
||||||
|
из ОЗУ-копии этой же страницы. Переворот всей страницы построчно — это
|
||||||
|
320×192 Б копирования КАЖДЫЙ кадр, что мимо бюджета на порядок.
|
||||||
|
|
||||||
|
**Варианты, которые надо будет взвесить (не сейчас):**
|
||||||
|
1. **Предпечённые перевёрнутые атласы.** Второй набор кадров
|
||||||
|
Кида/стража, перевёрнутый по вертикали ещё в `pop_pack_kid.py` (там уже
|
||||||
|
есть транспонирование — добавляется одной строкой). Рантайм: выбор
|
||||||
|
набора + зеркальная арифметика Y. Память: ещё ~28 страниц EMM при
|
||||||
|
бюджете ~3.3 МБ — не проблема. Похоже, самый дешёвый по тактам путь.
|
||||||
|
2. **Фон рисовать с обратным Y** — для row-major тайлов строка остаётся
|
||||||
|
непрерывным accel-прогоном, меняется только адрес назначения; цена —
|
||||||
|
вызов на строку вместо вызова на тайл. Померить, прежде чем закладывать.
|
||||||
|
3. **Аппаратная помощь** — до проектирования проверить, есть ли у
|
||||||
|
акселератора направление копирования «вниз» (обратный инкремент адреса);
|
||||||
|
если есть, вариант 1 может и не понадобиться. Смотреть
|
||||||
|
`docs/new/06-accel.md` и `docs/reference/accel_r.txt`.
|
||||||
|
|
||||||
|
**Почему не сейчас.** Уровни 1..8 этого не требуют, а к уровню 9 у нас уже
|
||||||
|
будет ответ на вопрос «сколько стоит кадр» (задачи CLIP-1/T-2) — без него
|
||||||
|
выбирать между вариантами выше бессмысленно.
|
||||||
|
|
||||||
|
## Заменить генератор псевдослучайных чисел
|
||||||
|
|
||||||
|
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
|
||||||
|
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
|
||||||
|
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
|
||||||
|
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
|
||||||
|
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
|
||||||
|
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
|
||||||
|
|
||||||
|
## Отключать мышь на время игры
|
||||||
|
|
||||||
|
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
|
||||||
|
см. memory `mouse_api`). Игре она не нужна вообще: управление —
|
||||||
|
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
|
||||||
|
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
|
||||||
|
который у нас и без того занят на 86 %.
|
||||||
|
|
||||||
|
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
|
||||||
|
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
|
||||||
|
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
|
||||||
|
кадров подряд прошли без единого превышения. То есть эффект реальный и
|
||||||
|
измеримый, просто в тесте он был наведён извне.
|
||||||
|
|
||||||
|
**Что проверить.**
|
||||||
|
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
|
||||||
|
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
|
||||||
|
«hide cursor» и снимает ли она обработчик.
|
||||||
|
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
|
||||||
|
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
|
||||||
|
3. Не ломает ли отключение выход в DSS: состояние обязано
|
||||||
|
восстанавливаться при `exit`, включая аварийный (atexit).
|
||||||
|
|
||||||
|
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
|
||||||
|
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
|
||||||
|
после выхода — заметный. Делать после того, как закроем стражей и
|
||||||
|
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||||||
|
возможный возврат `pop_bg` в резидент `--w3`).
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
||||||
|
|
||||||
|
> **Замер 2026-08-01 (актуальная сборка).** `_CODE` 25 119 Б, `_DATA` 3 709,
|
||||||
|
> куча ~2.4 КБ. Банки: 1 (`guards.c`) 1 896, 2 (`pop_bg.c`) 13 792,
|
||||||
|
> 3 (`pop_map.c`) 6 331, 4 (`pop_gdraw.c`) 2 236 — все из 16 384.
|
||||||
|
> **Резидента `--w3` больше нет**: отрисовка уехала в банк 2, и это сняло
|
||||||
|
> главное ограничение резидента (из банка его было не достать) — банк→банк
|
||||||
|
> работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража
|
||||||
|
> вынесена из банка 2 в собственный банк 4, потому что банк 2 подошёл к
|
||||||
|
> потолку (16 021 из 16 384) — коммит `2f3e854`.
|
||||||
|
>
|
||||||
|
> **Свободного места в банке 2 осталось ~2.6 КБ**, а туда же просятся
|
||||||
|
> чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать
|
||||||
|
> `levels_plan.md` §3 — посчитать, куда это ляжет. Свободные номера банков
|
||||||
|
> есть, гранулярность — файл.
|
||||||
|
>
|
||||||
|
> Из плана ниже **не сделаны шаги 5 (данные: `room_modif`, `dl1/dl2`,
|
||||||
|
> `_kbdraw_down`; потенциал ~1.5 КБ) и 7 (дедуп `draw_tile`, отложен по
|
||||||
|
> решению пользователя)**.
|
||||||
|
|
||||||
|
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||||
|
Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
|
||||||
|
как полностью перекрытый этим документом. Документ самодостаточный
|
||||||
|
— рассчитан на старт с пустого контекста.
|
||||||
|
|
||||||
|
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
||||||
|
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
|
||||||
|
ограничения окна W3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)
|
||||||
|
|
||||||
|
| Шаг | Статус | Факт |
|
||||||
|
|-----|--------|------|
|
||||||
|
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
|
||||||
|
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | −41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
|
||||||
|
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
|
||||||
|
| 4. Разгрузка/перебалансировка W3 | **сделан** | резидент = `pop_bg` + `pop_gdraw` (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б) |
|
||||||
|
| 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
|
||||||
|
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
|
||||||
|
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
|
||||||
|
|
||||||
|
**Замер сейчас против замера §1:** `_CODE` 24 881 → 24 421, куча W2 2 076 → 2 592 Б,
|
||||||
|
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
|
||||||
|
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
|
||||||
|
|
||||||
|
**Замер 2026-07-29 (после стража).** Появление стража съело кучу до 805 Б;
|
||||||
|
разгрузка — вынос ОТРИСОВКИ стража в резидент (`pop_gdraw.c`, `--w3`), логика
|
||||||
|
и состояние остались в W1/W2, чтобы банк `guards.c` их видел (R2). Итог:
|
||||||
|
`_CODE` 26 149 → 25 703, куча **805 → 1 245 Б**, W3-резидент 11 656 → 12 819
|
||||||
|
(свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент»
|
||||||
|
теперь формулируется одним правилом: **резидент = только то, что рисует и
|
||||||
|
зовётся исключительно из главного цикла**; всё, что может понадобиться банку,
|
||||||
|
остаётся в W1/W2.
|
||||||
|
|
||||||
|
### Что сделано вместо §5.3 (вынос loose в W3)
|
||||||
|
|
||||||
|
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
|
||||||
|
**порт архитектуры оригинала**: логика ставит пометку, отрисовка идёт
|
||||||
|
отдельным проходом — `set_redraw_*` (seg007) + `redraw_needed` (seg008:0178).
|
||||||
|
Появился `pop_redraw.c/.h`; `pop_trob` и `pop_map` больше не рисуют тайлы.
|
||||||
|
Наши самодельные счётчики (`spike_rest`, `button_rest`, `ldoor_rest`,
|
||||||
|
`loose_bake`, `loose_rest`, `ceil_rest`, `ceil_bake`, `land_bake`) удалены —
|
||||||
|
их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.
|
||||||
|
|
||||||
|
**Два исключения остались** (обе — функции ТОЛЬКО главного цикла, звать из
|
||||||
|
банка нельзя):
|
||||||
|
- `pop_process_trobs` — пламя факела и пузырёк зелья (покадровый оверлей);
|
||||||
|
- `pop_loose_tick` — падающий кусок (mob): spawn/tick/pos. В оригинале это
|
||||||
|
отдельная подсистема (`mobs` + `draw_moving`), разделение на логику и
|
||||||
|
отрисовку — задел следующей фазы.
|
||||||
|
|
||||||
|
### Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА
|
||||||
|
|
||||||
|
`tests/w3bankgfx` (huge + `--w3 res.c` + `--bank 1=bank1.c`, графика 256):
|
||||||
|
|
||||||
|
- банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри
|
||||||
|
банка до блита, после блита и после возврата из вызванной им W1/W2-функции
|
||||||
|
одна и та же (0xF0), резидент — 0xF3. То есть `_bgi_begin`/`_bgi_end`
|
||||||
|
корректно возвращают ИМЕННО банковую страницу (правило R4);
|
||||||
|
- вызов W1/W2-функции из банка работает, и она тоже может рисовать;
|
||||||
|
- резидент W3 жив и вызывается после возврата из банка (R3).
|
||||||
|
|
||||||
|
**Дополнительно выяснено (важно для стражей):** писучие статики
|
||||||
|
`__banked`-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не
|
||||||
|
прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё
|
||||||
|
состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в
|
||||||
|
W1/W2 как обычные глобалы, а банк — только код.
|
||||||
|
|
||||||
|
Ещё одна мина, найденная там же: инлайновый `in a,(#0xE2)` посреди тела
|
||||||
|
функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет
|
||||||
|
заливки стал номером страницы, и «резидент не рисовал»). Читать порт
|
||||||
|
отдельной `__naked`-функцией.
|
||||||
|
|
||||||
|
### Найденная по дороге ловушка компилятора
|
||||||
|
|
||||||
|
`(const T *)КОНСТАНТА + var*K` SDCC 4.5 может собрать неверно: умножение
|
||||||
|
делает в 16 битах, а потом берёт только младший байт (`ld c,l` / `inc b`).
|
||||||
|
Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага
|
||||||
|
пропадал `FRAME_NEEDS_FLOOR` и персонаж проваливался сквозь пол. Лечение —
|
||||||
|
считать адрес в `uint16_t` и кастовать один раз. Тот же паттерн в
|
||||||
|
`pop_level.c` компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа
|
||||||
|
работает» нельзя. Подробности: memory `sdcc_z80_const_ptr_index_bug`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Что уже сделано из v1 (не повторять)
|
||||||
|
|
||||||
|
- `--opt-code-size` и `--max-allocs 100000` **уже включены по умолчанию** в
|
||||||
|
`bin/sprinter-cc` (v1 §2.1 закрыт, выигрыш получен).
|
||||||
|
- Лишние блиты переднего слоя убраны (v1 §7 п.0): `fore_tile` больше не рисует
|
||||||
|
`bottom_id`, `_CODE` −388 Б.
|
||||||
|
- `gfx_blit_noclip` в libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.
|
||||||
|
- `--w3` как резидент окна 3 реализован и обкатан (memory `w3_resident_code`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)
|
||||||
|
|
||||||
|
Команда: `--memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c`.
|
||||||
|
|
||||||
|
### 1.1 Окна
|
||||||
|
|
||||||
|
| Область | Занято | Свободно | Примечание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| W1+W2 `_CODE` | 24 881 Б | — | 0x4100…0xA231 |
|
||||||
|
| W1+W2 `_HOME`+`_GSINIT`+`_DATA`+`_BSS` | ~4 240 Б | — | до 0xB2E4 |
|
||||||
|
| **W1+W2 куча** | 0 (никто не malloc'ит) | **2 076 Б** | 0xB2E4…0xBB00 |
|
||||||
|
| W1+W2 стек | — | 1 279 Б | 0xBB00…0xBFFE |
|
||||||
|
| **W3 резидент** | 14 376 Б | **2 008 Б** | 0xC000…0xF828 |
|
||||||
|
| EMM-страницы | 37 атласов + 1 уровень | ~215 страниц свободно | `sprinter_emm_budget` |
|
||||||
|
|
||||||
|
**Итого запаса до стены: ≈ 4 КБ** (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда
|
||||||
|
не влезут.
|
||||||
|
|
||||||
|
### 1.2 Код по модулям (точно, из `.rel`)
|
||||||
|
|
||||||
|
```
|
||||||
|
W1/W2 (_CODE 24 881): W3 резидент (_W3CODE 14 376):
|
||||||
|
pop_kid 6 963 pop_bg 11 643
|
||||||
|
pop_map 6 157 pop_trob 2 733
|
||||||
|
roomtest 2 583
|
||||||
|
pop_level 1 425
|
||||||
|
pop_ctrl 1 145
|
||||||
|
crt0 333
|
||||||
|
libc+libbgi ~6 275
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Крупнейшие функции/данные (из `.lst`)
|
||||||
|
|
||||||
|
```
|
||||||
|
pop_bg : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
|
||||||
|
mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
|
||||||
|
fore_only_tile 409, climb_overlay_tile 391, tile_table 371
|
||||||
|
pop_map : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
|
||||||
|
get_tile 266, do_knock 243, check_press 238, check_leave 212
|
||||||
|
pop_kid : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
|
||||||
|
(в _CODE!), собственно кода ~3.2 КБ
|
||||||
|
roomtest : enter_room+main ~2.1 КБ
|
||||||
|
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 `_DATA` (3 710 Б)
|
||||||
|
|
||||||
|
```
|
||||||
|
pop_trob 963 (room_modif[24][30] = 720 + trobs + rest-массивы)
|
||||||
|
pop_level 515 (копии LINKLOC/LINKMAP уровня)
|
||||||
|
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
|
||||||
|
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 Находки замера (мелкие, но чинить)
|
||||||
|
|
||||||
|
1. **`--w3` берёт ОДИН файл на флаг.** В `Makefile` написано
|
||||||
|
`--w3 pop_trob.c pop_map.c --w3 pop_bg.c`, и это значит «W3 = pop_trob и
|
||||||
|
pop_bg», а `pop_map.c` компилируется как обычный исходник в W1/W2. Судя по
|
||||||
|
`.sprinter-cc-roomtest/w3_pop_map.rel` (устаревший артефакт), когда-то
|
||||||
|
pop_map был в W3. **Решить осознанно** (см. §4) и записать явно:
|
||||||
|
`--w3 pop_trob.c --w3 pop_bg.c`.
|
||||||
|
2. `libc` тянет `_irq_state` 818 Б + `_kbdraw_state` 515 Б в `_DATA`.
|
||||||
|
`__irq_vec_buf` (513 Б) — таблица векторов IM2; `__kbdraw_down` (512 Б) —
|
||||||
|
битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ
|
||||||
|
в самом дефицитном окне.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА
|
||||||
|
|
||||||
|
Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по
|
||||||
|
`bin/sprinter-cc` (справка `--w3`/`--bank`) и по коду libbgi.
|
||||||
|
|
||||||
|
**(R1) Резидент W3 (`--w3`) и банки W3 (`--bank`) делят одно окно.**
|
||||||
|
Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова
|
||||||
|
`__banked` подменяет страницу W3 на банковую и возвращает резидентную назад.
|
||||||
|
|
||||||
|
**(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже.**
|
||||||
|
Пока исполняется банк, резидентной страницы в адресном пространстве нет.
|
||||||
|
Значит нельзя не только `bank → pop_bg()`, но и `bank → pop_map() → pop_bg()`.
|
||||||
|
**Это ключевое ограничение при выборе, что делать банком.**
|
||||||
|
|
||||||
|
**(R3) Резидент → банк работает** (через трамплин в W1), резидент → W1/W2 —
|
||||||
|
тоже.
|
||||||
|
|
||||||
|
**(R4) Графические примитивы libbgi звать можно откуда угодно.**
|
||||||
|
`_bgi_begin` читает текущую страницу W3 из порта 0xE2, а `_bgi_end` её
|
||||||
|
возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя
|
||||||
|
только одно: **звать `_bgi_begin`/`_bgi_end` ИЗ кода, который сам лежит в W3**
|
||||||
|
(после подмены страницы исчезнет исполняемый код — проверено, белый экран).
|
||||||
|
Поэтому `pop_bg` (резидент W3) обязан пользоваться готовыми примитивами
|
||||||
|
(`gfx_blit*`, `bar`, …), а батчинг скобки на весь `draw_tile` (v1 §8 шаг 1)
|
||||||
|
для него **невозможен** без переноса самого `draw_tile` в W1/W2.
|
||||||
|
|
||||||
|
**(R5) `--w3` кладёт в W3 код И rodata модуля** (`--codeseg/--constseg
|
||||||
|
W3CODE`), а писучие статики оставляет в `_DATA` (W2). То есть `const`-таблицы
|
||||||
|
переносятся в W3 бесплатно вместе с модулем (так уже лежит `tile_table` 371 Б).
|
||||||
|
|
||||||
|
**(R6) Данные в EMM-странице читаются, только пока страница в окне.**
|
||||||
|
`gfx_w0_map(page)` / `gfx_w0_unmap()` — окно W0 (0x0000…0x3FFF), первые 0x100
|
||||||
|
занимает ISR-стаб. Так уже работает `pop_level`. Цена — пара `OUT` на
|
||||||
|
маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем
|
||||||
|
цикле.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)
|
||||||
|
|
||||||
|
- **Нельзя банковать `pop_bg`.** Он вызывается из pop_map, pop_trob, roomtest,
|
||||||
|
pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит
|
||||||
|
`tile_table` и всю отрисовку. Банк дал бы трамплин на каждый блит.
|
||||||
|
- **Нельзя банковать `pop_map`, пока `pop_map` зовёт `pop_bg`** (R2). Сейчас
|
||||||
|
зовёт: `pop_loose_tick` и компания (~30 вызовов графики).
|
||||||
|
- **Нельзя тащить `kid_frames` в EMM «в лоб»**: он читается несколько раз за
|
||||||
|
кадр из коллизии (`kid_cur_dx`/`kid_cur_flags` → `dx_weight`,
|
||||||
|
`char_x_forward_edge`, …). Нужен кэш кадра (см. §5.1) — иначе маппинг
|
||||||
|
страницы окажется в горячем пути.
|
||||||
|
- **Нельзя «причёсывать» семейство `draw_tile` ради экономии, не имея
|
||||||
|
пиксельного теста.** Мы неделю выравнивали слои по SDLPoP; любой рефактор
|
||||||
|
этой зоны проверять диффом страниц (заморозка кадра клавишей `1` + сравнение
|
||||||
|
VRAM обеих страниц, приём из memory `mame_mcp_bridge`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. ЦЕЛЕВАЯ РАСКЛАДКА
|
||||||
|
|
||||||
|
Принцип: **W3-резидент = «толстая графика, которую зовёт только главный цикл»;
|
||||||
|
W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки =
|
||||||
|
новая холодная логика (стражи, боёвка, будущие уровни)**.
|
||||||
|
|
||||||
|
```
|
||||||
|
W1/W2 (всегда отображено) W3 резидент (стр. 0xC000) Банки W3
|
||||||
|
────────────────────────── ───────────────────────── ─────────
|
||||||
|
libc + libbgi pop_bg (отрисовка тайлов) guards.c
|
||||||
|
pop_kid (интерпретатор+рисование) pop_trob (анимации тайлов) fight.c
|
||||||
|
pop_map (коллизия/физика/предметы) pop_loose.c (loose+потолок) debug/roomnav
|
||||||
|
pop_geom (общая геометрия/тайлы) enter_room-часть roomtest?
|
||||||
|
pop_ctrl (ввод/диспетчер)
|
||||||
|
roomtest (главный цикл)
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему так:
|
||||||
|
|
||||||
|
- **`pop_map` остаётся в W1/W2** — его зовут и главный цикл, и (в будущем)
|
||||||
|
банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо
|
||||||
|
вынести графическую часть (loose/потолок) — она уезжает в W3 к `pop_bg`
|
||||||
|
(§5.3). После выноса `pop_map` становится **чистой логикой без единого
|
||||||
|
вызова графики** — тот самый bank-safe API.
|
||||||
|
- **`pop_kid` остаётся в W1/W2**: `play_seq`/`kid_set_seq`/`Kid` нужны и
|
||||||
|
стражам (у стражей ТА ЖЕ seqtbl), а `kid_draw` зовёт только libbgi (R4).
|
||||||
|
- **`pop_trob` остаётся резидентом**: его зовёт только главный цикл, и он сам
|
||||||
|
зовёт `pop_bg` — идеальный житель W3.
|
||||||
|
- **Банк стражей не зовёт ничего из W3.** Рисование стражей — либо через
|
||||||
|
libbgi напрямую (R4), либо (лучше) резидентный `guard_draw()` в W1/W2 рядом
|
||||||
|
с `kid_draw`, а банк только считает состояние. Тот же приём мы уже
|
||||||
|
используем для `pop_item_taken`/`pop_loose_fell`/`pop_ceil_fell`: банк
|
||||||
|
выставляет флаг — резидент рисует.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. ПЛАН РАБОТ
|
||||||
|
|
||||||
|
Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место
|
||||||
|
следующему.
|
||||||
|
|
||||||
|
### Шаг 1. `kid_data.h` (3 745 Б) → EMM-страница + порт `load_frame` — **самый большой выигрыш**
|
||||||
|
|
||||||
|
Сейчас `kid_seqtbl` (2310) + `kid_frames` (1205) + `kid_seq_off` (230) лежат в
|
||||||
|
`_CODE` окна W1/W2 — это 15 % всего дефицитного пространства.
|
||||||
|
|
||||||
|
Как переносить:
|
||||||
|
|
||||||
|
1. `pop_extract_kid_data.py` дополнительно пишет `kid_data.bin` (те же три
|
||||||
|
таблицы подряд, фиксированные смещения).
|
||||||
|
2. Грузим её в отдельную EMM-страницу тем же способом, что уровень
|
||||||
|
(`pop_level_load` — готовый образец), хэндл держим в `pop_kid`.
|
||||||
|
3. **Порт `load_frame` (seg006) и глобала `cur_frame`** — в оригинале ровно
|
||||||
|
так и сделано: раз за тик кадр копируется в структуру, а весь остальной код
|
||||||
|
читает `cur_frame`, а не таблицу. У нас `kid_cur_dx()/kid_cur_flags()`
|
||||||
|
станут чтением из `cur_frame` (5 байт в `_DATA`).
|
||||||
|
4. `play_seq` оборачивается в один `gfx_w0_map(kid_data_page)` … `unmap` на
|
||||||
|
вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).
|
||||||
|
|
||||||
|
Выигрыш: **−3 745 Б из W1/W2**, цена — один маппинг страницы за тик и 5 байт
|
||||||
|
`_DATA`. Дополнительный бонус: `load_frame`/`cur_frame` — шаг К СХОДСТВУ с
|
||||||
|
оригиналом, а не отход от него.
|
||||||
|
|
||||||
|
Риск: сломать `play_seq` (сердце анимации). Проверка: прогон по комнатам с
|
||||||
|
эталонными позами (вис, подтягивание, прыжки, подъём меча).
|
||||||
|
|
||||||
|
### Шаг 2. Модуль `pop_geom.c` — дедуп + bank-safe фундамент
|
||||||
|
|
||||||
|
Сейчас продублировано между модулями:
|
||||||
|
|
||||||
|
| что | где | сколько |
|
||||||
|
|---|---|---|
|
||||||
|
| `y_to_row`/`y_to_row_mod4` | pop_bg + pop_map | 2 копии |
|
||||||
|
| `char_dx_forward` | pop_kid + pop_map | 2 копии |
|
||||||
|
| `x_bump[20]` | pop_kid (uint8) + pop_map (int16) | 20 + 40 Б, РАЗНЫЕ типы |
|
||||||
|
| `y_land[5]` | pop_kid + pop_map | 10 + 10 Б |
|
||||||
|
| `tile_is_floor` | pop_map (+ проверка кодов в roomtest) | 2 места |
|
||||||
|
| 32-битный LCG `prandom` | pop_bg (`prandom`) + pop_trob (`trob_prandom`) | 2 копии по ~60 Б + 2 сида |
|
||||||
|
|
||||||
|
Собрать в один W1/W2-модуль `pop_geom.c`: таблицы `x_bump/y_land/dir_front/
|
||||||
|
dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
|
||||||
|
`tile_is_floor`, `prandom`. Выигрыш прямой — сотни байт (оценка 250–400 Б),
|
||||||
|
но главное — **это и есть тот «чистый» API, который потом сможет звать банк**
|
||||||
|
(R2): вся геометрия оказывается в W1/W2 по определению.
|
||||||
|
|
||||||
|
Осторожно: `prandom` у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными
|
||||||
|
сидами (стены vs фазы факелов). Объединять функцию можно, **сиды — нет**:
|
||||||
|
передавать сид указателем/по индексу, иначе поедет раскладка кладки.
|
||||||
|
|
||||||
|
### Шаг 3. Вынести loose/потолок из `pop_map` в W3
|
||||||
|
|
||||||
|
`pop_map` — единственный модуль W1/W2, который зовёт графику, и делает это
|
||||||
|
ровно в одном логическом блоке: `pop_loose_tick` + `check_press` + `do_knock` +
|
||||||
|
`fell_on_your_head` + `check_loose_fall_on_kid` + плита-потолок (~1.4–2 КБ).
|
||||||
|
|
||||||
|
Вынести их в `pop_loose.c`, собираемый `--w3` рядом с `pop_bg`/`pop_trob`.
|
||||||
|
Тогда:
|
||||||
|
|
||||||
|
- `pop_map` = чистая логика (bank-safe, R2 соблюдён);
|
||||||
|
- W1/W2 худеет ещё на ~1.5–2 КБ;
|
||||||
|
- W3 растёт на столько же — а место там появится после шага 4.
|
||||||
|
|
||||||
|
### Шаг 4. Перебалансировка резидента W3
|
||||||
|
|
||||||
|
После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:
|
||||||
|
|
||||||
|
1. **`wall_pattern` (944 Б) + `mob_render`/`mob_tick_one` (1381 Б)** — кандидаты
|
||||||
|
на переезд в W1/W2: их зовёт только `pop_bg`/`pop_loose`, но сами они уже
|
||||||
|
пользуются только libbgi (R4), значит из W1/W2 работают и остаются
|
||||||
|
достижимыми из банка.
|
||||||
|
2. `tile_table` и мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в
|
||||||
|
EMM-страницу **уровня** (там ~13.8 КБ свободно) — но только если чтение
|
||||||
|
происходит под уже замапленной страницей. Сейчас `draw_tile` читает
|
||||||
|
`tile_table` ВНЕ W0-контекста → потребуется явный маппинг на тайл. **Не
|
||||||
|
делать раньше замера**: 30 тайлов на входе в комнату × map/unmap — терпимо,
|
||||||
|
а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.
|
||||||
|
3. Если и этого мало — `enter_room` (~2.1 КБ, зовётся только при смене комнаты)
|
||||||
|
переносится в резидент W3 или в БАНК (он вызывается из главного цикла =
|
||||||
|
резидента, значит банк допустим по R3).
|
||||||
|
|
||||||
|
### Шаг 5. Данные
|
||||||
|
|
||||||
|
1. **`room_modif[24][30]` = 720 Б** (pop_trob, `_DATA`). Нужен произвольный
|
||||||
|
доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты ×
|
||||||
|
30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей
|
||||||
|
комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице
|
||||||
|
уровня, а в `_DATA` держать кэш на 2–3 комнаты (свою + левого/правого
|
||||||
|
соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена —
|
||||||
|
аккуратность на швах (кнопка в одной комнате открывает ворота в другой).
|
||||||
|
**Делать последним** — это самая «тонкая» правка по семантике.
|
||||||
|
2. **`dl1[256]`+`dl2[256]` = 512 Б** (pop_level, `_DATA` — копии LINKLOC/
|
||||||
|
LINKMAP уровня) — читаются при нажатии кнопки
|
||||||
|
и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы
|
||||||
|
уровня (она и так маппится) — но проверить, что `pop_doorlink2` не зовётся
|
||||||
|
из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б.
|
||||||
|
3. **`_kbdraw_down[512]` 512 Б** (libc): проверено — это **байт на скан-код**
|
||||||
|
(`libc/kbd/_kbdraw_state.c`), хотя комментарий называет его битовой картой.
|
||||||
|
Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и в
|
||||||
|
`kbd_raw_down`. Трогать осторожно: raw-клавиатура уже дважды была
|
||||||
|
источником залипаний (memory `kbd_raw_fifo_drain`,
|
||||||
|
`kbd_overrun_wipe_modifiers`) — правку сопровождать прогоном docs/kbd-games.
|
||||||
|
4. **`__irq_vec_buf` 513 Б** (libc IM2): таблица векторов обязана быть
|
||||||
|
выровнена и полна — не трогать.
|
||||||
|
|
||||||
|
### Шаг 6. Контракт банка стражей (проектируется ДО написания кода)
|
||||||
|
|
||||||
|
Когда дойдём до стражей:
|
||||||
|
|
||||||
|
- `guards.c` собирается `--bank 1=guards.c`, режим `huge` (или `big` с
|
||||||
|
`BANKED=W1`, если W3 окажется тесен для трамплинов).
|
||||||
|
- **Банк зовёт только:** `pop_map` (чистая логика после шага 3), `pop_kid`
|
||||||
|
(`play_seq`, `kid_set_seq`, `cur_frame`), `pop_geom`, libc/libbgi.
|
||||||
|
- **Банк НЕ зовёт:** `pop_bg`, `pop_trob`, `pop_loose` (резидент W3) — ни
|
||||||
|
прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг,
|
||||||
|
рисует резидент (идиома `pop_item_taken`).
|
||||||
|
- Первым делом — **пробник** (`tests/` или `--bank` на пустышке): банк зовёт
|
||||||
|
`pop_map`-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка
|
||||||
|
W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не
|
||||||
|
веры в неё.
|
||||||
|
|
||||||
|
### Шаг 7. Мелкий дедуп в `pop_bg` (после того, как появится тест страниц)
|
||||||
|
|
||||||
|
- Пять функций-редроев (`pop_spike_redraw`, `pop_loose_shake_draw`,
|
||||||
|
`pop_floor_bake`, `pop_button_redraw`, `pop_leveldoor_redraw`) отличаются
|
||||||
|
только прямоугольником heal, банком и набором тайлов — свести к одному
|
||||||
|
параметризованному хелперу (оценка −150…250 Б).
|
||||||
|
- `env_b/wall_b/fore_b/pot_b` — четыре одинаковых обёртки над `blit_b`
|
||||||
|
(оставить: экономия единицы байт, читаемость дороже).
|
||||||
|
- `overlay_mid_tile` / `fore_only_tile` / `climb_overlay_tile` / `draw_tile`
|
||||||
|
— общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои».
|
||||||
|
Тут экономия потенциально сотни байт, но это **та самая зона риска из §3** —
|
||||||
|
только с пиксельным диффом до/после и по одному слою за раз.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Ожидаемый итог
|
||||||
|
|
||||||
|
| Шаг | W1/W2 | W3 | Риск |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1. kid_data → EMM + load_frame | **−3 745** | — | средний (сердце анимации) |
|
||||||
|
| 2. pop_geom (дедуп) | −250…400 | — | низкий |
|
||||||
|
| 3. loose → W3 | −1 500…2 000 | +1 500…2 000 | низкий (перенос как есть) |
|
||||||
|
| 4. разгрузка W3 (wall_pattern, mob) | +2 300 | −2 300 | низкий |
|
||||||
|
| 5. данные (room_modif, LINKLOC, kbd) | −1 000…1 600 | — | средний/высокий |
|
||||||
|
| 7. дедуп редроев pop_bg | — | −150…250 | средний |
|
||||||
|
|
||||||
|
Суммарно: **W1/W2 освобождается ~4.5–6 КБ**, W3 остаётся примерно в нынешнем
|
||||||
|
объёме, но становится «правильно заполненным» — в нём только то, что банк
|
||||||
|
никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают
|
||||||
|
до 16 КБ на банк, не трогая резидент.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Как мерить и проверять (обязательно к каждому шагу)
|
||||||
|
|
||||||
|
1. **До/после по `.rel`** — точные размеры на модуль:
|
||||||
|
`for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done`
|
||||||
|
(области `_CODE`/`_W3CODE`/`_DATA`). Итоги окон печатает сам `sprinter-cc`.
|
||||||
|
2. **Функции** — из `.lst` (метки `_name:` и адреса), скрипт в истории этой
|
||||||
|
сессии; полезно ловить «функция распухла после рефактора».
|
||||||
|
3. **MAME**: любой перенос кода между окнами/страницами — это класс «молча
|
||||||
|
ломается» (`sprinter_memory_modes`). Минимум: комната 1 (loose), 12
|
||||||
|
(вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота).
|
||||||
|
4. **Пиксельный дифф** для правок отрисовки: заморозить кадр (`1`), сравнить
|
||||||
|
обе страницы дабл-буфера через `vram` (см. memory `mame_mcp_bridge`) и/или
|
||||||
|
сверить с эталонным рендером `render_room.py`.
|
||||||
|
5. **Скорость** — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE
|
||||||
|
(приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.
|
||||||
|
|
||||||
|
## 8. Ссылки
|
||||||
|
|
||||||
|
- `bin/sprinter-cc` — справка по `--w3`, `--bank`, `--memory`, `--memory-manual`.
|
||||||
|
- `runtime/crt0_banked.s`, `runtime/bank.s` — трамплины и захват резидентной
|
||||||
|
страницы W3.
|
||||||
|
- `libbgi/common/_bgi_begin.c` / `_bgi_end.c` — механика скобки W3 (R4).
|
||||||
|
- memory: `w3_resident_code`, `pop_banking_architecture`, `sdcc_banking`,
|
||||||
|
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
||||||
|
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
||||||
|
`libc_one_function_per_module`.
|
||||||
|
- `applications/PoP/roomtest/TASKS.md` — что из этого берётся в работу сейчас.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Скорость отрисовки: замеры и запас
|
||||||
|
|
||||||
|
Перенесено из удалённого `size_optimization_plan.md` §8 (замер 2026-07-27) —
|
||||||
|
единственная его часть, которая не была перекрыта этим документом.
|
||||||
|
Профилирование в MAME: маркеры в порт 0xFE + `wpiset … totalcycles` (приём из
|
||||||
|
memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже стоят полосы
|
||||||
|
бордюра `PROF()`. Кадр Sprinter = **430 080 тактов**.
|
||||||
|
|
||||||
|
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
|
||||||
|
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
|
||||||
|
пересчёт src, нарезка полос >256), а не за пиксели:
|
||||||
|
|
||||||
|
| путь (спрайт 32×3) | тактов |
|
||||||
|
|---|---|
|
||||||
|
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
|
||||||
|
| линейное ядро без клипа | 4 617 |
|
||||||
|
|
||||||
|
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
|
||||||
|
кадра**; сам `bar` — только 13 308.
|
||||||
|
|
||||||
|
**Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на
|
||||||
|
него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже
|
||||||
|
тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`).
|
||||||
|
**Не закрыт heal** — задача CLIP-1 в `../roomtest/TASKS.md`.
|
||||||
|
|
||||||
|
**ВАЖНО:** W3-скобку (`_bgi_begin`/`_bgi_end`) ставит САМА libbgi — вызывать
|
||||||
|
её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
|
||||||
|
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
|
||||||
|
белый экран).
|
||||||
|
|
||||||
|
**Запас, когда перестанет хватать бюджета кадра:**
|
||||||
|
|
||||||
|
1. **Батчинг W3-скобки** — одна `_bgi_begin`/`_bgi_end` на весь `draw_tile`
|
||||||
|
вместо скобки на блит; нужен публичный batch-API в libbgi.
|
||||||
|
**Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`,
|
||||||
|
длинная серия задержит кадровое прерывание — а по разбору KBD-1
|
||||||
|
(`../roomtest/TASKS.md`) длинные DI-окна и есть причина потери байт
|
||||||
|
клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую.
|
||||||
|
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
|
||||||
|
(до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
|
||||||
|
`gfx_blit_part` с обрезкой по фазе `gate_bot_y & 7`: 7 блитов → 1.
|
||||||
|
3. **Не перерисовывать статичные части шва** — грань ворот, пол и кромка при
|
||||||
|
анимации решётки не меняются (см. OPT-1 в `../roomtest/bug_closed.md` —
|
||||||
|
решено не делать, стоимость транзиентная).
|
||||||
|
4. **T-1 / T-2** (`../roomtest/bug_list.md`) — перерисовка пик по причине и
|
||||||
|
idle-skip Кида: самый большой оставшийся резерв, потому что убирает работу
|
||||||
|
целиком, а не удешевляет её.
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты)
|
||||||
|
|
||||||
|
Статус: план, 2026-08-01. Продолжает `PORT_PLAN.md` §7 (Фаза 1: «переходы
|
||||||
|
между экранами» → теперь между УРОВНЯМИ). Текущая точка: `roomtest` играет
|
||||||
|
уровень 1 целиком в одной комнате-за-комнатой модели, но уровень нельзя
|
||||||
|
ни выбрать, ни закончить.
|
||||||
|
|
||||||
|
Источник истины — `../SDLPoP/src/` (правило `../CLAUDE.md`). Ключевые
|
||||||
|
места: `seg000.c: load_lev_spr/play_level_2/init_game`, `seg005.c:
|
||||||
|
up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`,
|
||||||
|
`seg002.c` (спецсобытия уровней), `data.h:835..850` (потабличные различия
|
||||||
|
уровней).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Что уже готово (не проектировать заново)
|
||||||
|
|
||||||
|
- **Формат и загрузчик уровня.** `pop_level.c/.h` читает сырой
|
||||||
|
`res200N.bin` (2305 Б) в отдельную EMM-страницу; путь — параметр
|
||||||
|
`pop_level_load(const char *)`. Мультиуровневость здесь стоит одной
|
||||||
|
функции формирования имени.
|
||||||
|
- **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`,
|
||||||
|
`pop_level_start_pos()`, `pop_level_start_dir()` — реализованы и пока
|
||||||
|
НЕ вызываются (см. `../roomtest/TASKS.md` L1-START).
|
||||||
|
- **Страж по данным уровня**: `pop_level_guard()` (порт `enter_guard`),
|
||||||
|
сохранение состояния между комнатами (`pop_guard_state_save`).
|
||||||
|
- **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py`
|
||||||
|
`build_palette`): env 0x50, wall 0x60, pot 0x40, kid 0x70, меч 0x80,
|
||||||
|
страж 0x90. Это тот же раскрой, что `set_pal_arr(0x50/0x60)` в
|
||||||
|
`seg000.c:1140..1148`, — значит смена тайлсета не требует переиндексации
|
||||||
|
спрайтов Кида (см. §3).
|
||||||
|
- **Все 16 файлов уровней распакованы**: `../SDLPoP/data/LEVELS/res2000..
|
||||||
|
res2015.bin` (0 — демо-уровень).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что реально различается между уровнями (замер по данным, не по памяти)
|
||||||
|
|
||||||
|
Таблицы из `../SDLPoP/src/data.h:840..847` + инвентарь тайлов, снятый прямо
|
||||||
|
с `res200N.bin` (маска `fg & 0x1F`):
|
||||||
|
|
||||||
|
| Ур. | Тайлсет | Страж | Новое против предыдущих |
|
||||||
|
|-----|---------|-------|--------------------------|
|
||||||
|
| 1 | dungeon | обычный | — (текущая база) |
|
||||||
|
| **2** | **dungeon** | **обычный** | **ничего нового: тот же набор объектов минус меч** |
|
||||||
|
| 3 | dungeon | СКЕЛЕТ | чомперы |
|
||||||
|
| 4 | palace | обычный | **тайлсет palace**, зеркало (спецсобытие `mirror_level`) |
|
||||||
|
| 5 | palace | обычный | — |
|
||||||
|
| 6 | palace | ТОЛСТЫЙ | падение на входе (спецсобытие) |
|
||||||
|
| 7 | dungeon | обычный | — |
|
||||||
|
| 8, 9 | dungeon | обычный | — |
|
||||||
|
| 10, 11 | palace | обычный | — |
|
||||||
|
| 12 | dungeon | ТЕНЬ | seamless-выход (комната 23), исчезающий меч |
|
||||||
|
| 13 | dungeon | ВИЗИРЬ | мышь, особый выход |
|
||||||
|
| 14 | palace | нет | — |
|
||||||
|
| 15 | dungeon | нет | финал |
|
||||||
|
|
||||||
|
Прямое следствие для порядка работ: **уровень 2 не требует ни одного нового
|
||||||
|
ассета и ни одной новой механики** — он проверяет ровно машинерию перехода.
|
||||||
|
Это и есть первый шаг.
|
||||||
|
|
||||||
|
Прочие потабличные различия, которые придётся завести массивами по 16:
|
||||||
|
`tbl_level_type` (тайлсет), `tbl_guard_type` (−1 = стражей нет),
|
||||||
|
`tbl_guard_hp`, `tbl_level_color` (вариантные палитры, 1.3), `tbl_entry_pose`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Шаг 1 — машинерия перехода (цель: уровни 1 → 2 → 3) — **СДЕЛАН 2026-08-04**
|
||||||
|
|
||||||
|
> **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 →
|
||||||
|
> Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие
|
||||||
|
> колонны рисуются) → уровень 3. Разбор что именно сделано и что по
|
||||||
|
> уровню 2 осталось — `../roomtest/TASKS.md`, запись **L2**.
|
||||||
|
>
|
||||||
|
> Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется:
|
||||||
|
> **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина
|
||||||
|
> двери уровня) и **большую склянку** `add_life` (тип зелья 2, комната 20).
|
||||||
|
|
||||||
|
Порядок именно такой; каждый пункт проверяем в MAME отдельно.
|
||||||
|
|
||||||
|
**2.1 Выход с уровня.** Портировать `up_pressed()` ветку двери
|
||||||
|
(`seg005.c:410..423`) + `go_up_leveldoor()` (`seg005.c:497`): дверь рядом
|
||||||
|
(при/за/перед персонажем) И `drawn_room != level.start_room` И створка
|
||||||
|
открыта полностью (`curr_room_modif >= 42` — вариант `fix_exit_door`) →
|
||||||
|
`Char.x = x_bump[...] + 10`, направление влево, `seq_70_go_up_on_level_door`.
|
||||||
|
Затем оживить опкод `0xF1 END_LEVEL` в `play_seq` (`../roomtest/pop_kid.c:418`
|
||||||
|
— сейчас пустой `break`): `++pop_next_level`, как `seg006.c:662`.
|
||||||
|
|
||||||
|
**2.2 Цикл уровня.** В `main()` после тика: `if (pop_next_level !=
|
||||||
|
pop_current_level) → load_level(pop_next_level)`. Порядок сноса/подъёма
|
||||||
|
состояния (порт `load_lev_spr` + `play_level_2`):
|
||||||
|
`pop_level_free` → `pop_level_load("LEVELS\\res200%d.bin")` →
|
||||||
|
`pop_trob_reset` → `pop_guard_reset` → сброс tile-override'ов
|
||||||
|
(`ovr_*` в `roomtest.c`) → `enter_room(pop_level_start_room())` →
|
||||||
|
`kid_init(поза/позиция/направление из данных уровня)`.
|
||||||
|
**HP через уровень переносится** (в оригинале `hitp_beg_lev`), не сбрасывать
|
||||||
|
в максимум — сверить с `seg000.c` `init_game`/`play_level_2`.
|
||||||
|
|
||||||
|
**2.3 Стражи по уровню.** Завести `tbl_guard_type[16]`/`tbl_guard_hp[16]`;
|
||||||
|
`-1` = стражей на уровне нет (уровни 14, 15) — `pop_guard_enter` обязан это
|
||||||
|
понимать, иначе на 14-м полезут стражи из мусора. Для шага 1 (уровни 2, 3)
|
||||||
|
достаточно обычного стража, но проверку `-1` заложить сразу.
|
||||||
|
|
||||||
|
**2.4 Чит «следующий уровень» (Shift+L).** Реализуется ровно тем же
|
||||||
|
`pop_next_level` — и без него отладка уровней превращается в прохождение
|
||||||
|
игры руками. Делать в этом же шаге, не позже (см. §4).
|
||||||
|
|
||||||
|
**Приёмка шага 1:** дверь уровня 1 → уровень 2 играется целиком → его дверь
|
||||||
|
→ уровень 3 стартует (чомперы могут быть ещё не портированы — тогда
|
||||||
|
фиксируем как известное ограничение, а не «баг»).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Шаг 2 — второй тайлсет (palace, уровни 4+)
|
||||||
|
|
||||||
|
**Ассеты.** `toolchain/pop_pack_bg.py` уже читает PNG каскадом
|
||||||
|
VDUNGEON→VPALACE (та же логика, что в игре), но печёт ОДИН набор атласов
|
||||||
|
(`pop_env0..4.atl`, `pop_wall.atl`, `pop_fore.atl` ≈ 75 КБ). Нужен второй
|
||||||
|
набор из VPALACE (`pal_env*.atl` / `pal_wall.atl`), плюс `torch_debris` —
|
||||||
|
тайл, который встречается только на palace-уровнях. По EMM это ещё ~6
|
||||||
|
страниц при бюджете ~3.3 МБ — не проблема.
|
||||||
|
|
||||||
|
**Палитра — главный технический вопрос, и он уже решён раскроем.**
|
||||||
|
Тайлсет живёт в слотах `0x50..0x5F` (env) и `0x60..0x6F` (wall); Кид, меч,
|
||||||
|
страж, склянки — в других слотах. Значит смена тайлсета = перезапись 32
|
||||||
|
записей палитры (`gfx_pal_set` на обе страницы, как `flash_bg` в
|
||||||
|
`roomtest.c`), а НЕ перезагрузка `kid.pal` и не переиндексация спрайтов.
|
||||||
|
Сделать `pal_dungeon.bin` / `pal_palace.bin` (по 32 записи) и грузить при
|
||||||
|
смене типа уровня. Проверить артефактом: скриншот palace-комнаты против
|
||||||
|
рендера `render_room.py` для того же уровня.
|
||||||
|
|
||||||
|
**Вариантные цвета уровней** (`tbl_level_color`, `level_var_palettes` — это
|
||||||
|
уже 1.3, в 1.0 их нет): по той же механике, тот же диапазон слотов. Решение
|
||||||
|
на будущее — сначала базовые два тайлсета, потом при желании цвета.
|
||||||
|
|
||||||
|
**Выбор набора в коде.** `pop_bg_load()` сейчас грузит фиксированные имена;
|
||||||
|
превратить в `pop_bg_load(type)` с двумя таблицами имён + выгрузка старых
|
||||||
|
атласов при смене типа (`atlas_free`). Переключение — только на границе
|
||||||
|
уровня, не в кадре.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Читы SDLPoP: что взять на следующем этапе
|
||||||
|
|
||||||
|
Из `../SDLPoP/README.md` (раздел Cheats). У нас уже есть: **K** — убить
|
||||||
|
стража, **I** — бессмертие (наш, в оригинале нет), **S** — выдать меч (наш),
|
||||||
|
**+/−** — обход комнат (`ROOMNAV`, наш).
|
||||||
|
|
||||||
|
**Брать сразу вместе с переходами уровней** (без них отладка дороже самой
|
||||||
|
работы):
|
||||||
|
|
||||||
|
| Чит | Что даёт | Цена |
|
||||||
|
|-----|----------|------|
|
||||||
|
| **Shift+L — следующий уровень** | единственный вменяемый способ тестировать уровни 2..15 | тривиально: `++pop_next_level` из §2.2 |
|
||||||
|
| **R — воскресить Кида** | у нас респавн по ↑ + таймаут; порт `resurrect` ближе к оригиналу и не мешает управлению | низкая |
|
||||||
|
| **Shift+S / Shift+T — +1 HP / +максимум** | отладка боёвки без «ровно трёх попыток»; честная замена нашему читу бессмертия | низкая, HP-машинерия уже есть |
|
||||||
|
| **[ и ] — сдвинуть Кида на пиксель** (debug-чит SDLPoP) | прямо бьёт в наш класс багов «окклюзия/шов на один пиксель» — воспроизведение позы без ловли момента | тривиально |
|
||||||
|
|
||||||
|
**Брать во вторую очередь:**
|
||||||
|
|
||||||
|
| Чит | Почему позже |
|
||||||
|
|-----|--------------|
|
||||||
|
| **H / J / U / N + Ctrl+B — смотреть соседние комнаты** | требует честной модели `drawn_room ≠ Kid.room` (наш S3-straddle, каркас есть: `update_kid_render_dx`). Зато потом заменяет самодельный `ROOMNAV` и попутно закрывает straddle-задачу |
|
||||||
|
| **Shift+W — медленное падение (feather)** | ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` уже есть, но не проверена ничем — чит станет её единственным тестом |
|
||||||
|
| **C / Shift+C — номера комнат** | у нас номер рисуется палочками именно потому, что текст тянет 2 КБ знакогенератора в W2 (`roomtest.c`). Ждёт своего шрифта |
|
||||||
|
|
||||||
|
**Не брать:** `Shift+I` (переворот экрана), `Shift+B` (blind mode) —
|
||||||
|
развлекательные, к отладке порта отношения не имеют. `−/+` (время) — нужен
|
||||||
|
таймер уровня, которого у нас нет (Фаза 6).
|
||||||
|
|
||||||
|
**Отдельно, дорого, но очень ценно — `F6`/`F9` (quicksave/quickload точного
|
||||||
|
состояния).** Это сериализация `Char` + `room_modif` всех комнат + trob'ов +
|
||||||
|
состояния стражей. Даёт то, чего нам сейчас сильно не хватает:
|
||||||
|
воспроизводимый регресс в MAME («вот кадр, где баг») вместо ручного подхода
|
||||||
|
к позиции. Кандидат сразу после того, как заработают уровни.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Риски и что проверить артефактом до кодинга
|
||||||
|
|
||||||
|
1. **Размер кода.** Замер сборки 2026-08-01: `_CODE` 25 119 Б,
|
||||||
|
куча ~2.4 КБ, банк 2 (`pop_bg`) 13 792 / 16 384, банк 3 (`pop_map`)
|
||||||
|
6 331, банк 1 (`guards`) 1 896, банк 4 (`pop_gdraw`) 2 236. Чомперы,
|
||||||
|
зеркало, скелет и второй тайлсет пойдут в банк 2 — там осталось 2.6 КБ.
|
||||||
|
**Прежде чем начинать §3, посчитать, куда лягут новые тайлы**, иначе
|
||||||
|
повторится история «банк 2 упёрся в потолок» (коммит 2f3e854). Свободные
|
||||||
|
номера банков есть (5+), гранулярность — файл.
|
||||||
|
2. **Спецсобытия уровней** (`seg002.c`: `level3_set_chkp`, `sword_disappears`,
|
||||||
|
`Jaffar_exit`, зеркало, мышь) — их НЕ надо портировать заранее. Для
|
||||||
|
уровней 2 и 3 нужен только чекпойнт уровня 3. Остальное — по мере
|
||||||
|
подхода к уровню.
|
||||||
|
3. **Чомперы** (уровень 3 и почти все дальше) — отдельная механика
|
||||||
|
(`animate_chomper` + коллизия + смерть); шаблон работы тот же, что у
|
||||||
|
пик/ворот, см. `gates_spikes_plan.md`.
|
||||||
|
4. **`tbl_guard_type = -1`** на уровнях 14/15: без проверки страж
|
||||||
|
«появится» из неинициализированных данных.
|
||||||
|
5. **Уровень 0 (демо)** существует в данных, но в скоуп не входит.
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# Loose floors (проваливающиеся полы) — план порта
|
|
||||||
|
|
||||||
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose). ПЛАН, ещё не
|
|
||||||
реализовано. Тайл в комнате 1: `[2,6] = 0x0B = tiles_11_loose`.
|
|
||||||
|
|
||||||
## 1. Хранение состояния
|
|
||||||
|
|
||||||
- **Тип тайла**: `curr_room_tiles[tilepos] & 0x1F == 11` (tiles_11_loose).
|
|
||||||
Бит `0x20` = «solid» loose (авто-падающий вариант, ур.13 — от шага НЕ
|
|
||||||
падает). После падения тайл → `0` (tiles_0_empty).
|
|
||||||
- **Модификатор** `curr_room_modif[tilepos]` = состояние анимации:
|
|
||||||
- `0` — покой (обычный loose-пол);
|
|
||||||
- `0x80..0x83` — **трясётся** (бит7); за ~4 кадра затухает обратно в 0;
|
|
||||||
- `1..11` — **обратный отсчёт до падения** (на нём что-то стоит);
|
|
||||||
достигает `loose_floor_delay = 11` → падает.
|
|
||||||
|
|
||||||
## 2. Анимация тряски (shake) — когда включается
|
|
||||||
|
|
||||||
- **Триггер = do_knock** (seg007:0FE0): на ЖЁСТКОМ приземлении в кадрах
|
|
||||||
посадки играет `SEQ_KNOCK_DOWN` → взводит `knock` → `check_knock()` →
|
|
||||||
`do_knock(room, curr_row − (knock>0))`.
|
|
||||||
- `do_knock(room, row)`: по всем колонкам ряда — если тайл loose →
|
|
||||||
`loose_make_shake()`.
|
|
||||||
- `loose_make_shake()` (seg007:0FB4): если `modif==0` (и не ур.13) →
|
|
||||||
`modif = 0x80`, `add_trob(type 1)`.
|
|
||||||
- **Отсюда кейс пользователя**: Kid падает/приземляется на `[2,4]` →
|
|
||||||
do_knock трясёт ВСЕ loose-тайлы ряда 2 → `[2,6]` трясётся. (Через
|
|
||||||
knock-смещение ряда может задеть и соседний ряд.)
|
|
||||||
- `animate_loose` (кадрово): `++modif`; при бите7 трясёт до `>=0x84` →
|
|
||||||
сброс в 0, `trob.type=-1`. `loose_shake()` играет звук
|
|
||||||
(sound 20/21/22) по таблице `loose_sound[]`.
|
|
||||||
|
|
||||||
## 3. Анимация падения (fall) — когда включается
|
|
||||||
|
|
||||||
- **Триггер = make_loose_fall(1)** (seg007:0EF6), вызывается когда:
|
|
||||||
- Kid СТОИТ на loose-тайле — `check_press()` (seg006): кадр с
|
|
||||||
FRAME_NEEDS_FLOOR над loose → make_loose_fall(1);
|
|
||||||
- зацеп/подтягивание на loose (`check_grab`, `check_jump_up`);
|
|
||||||
- пробой сверху: кадр 79 (jumphang) над loose → make_loose_fall(1);
|
|
||||||
- авто-падающие (ур.13) — `make_loose_fall(-(prandom&0x0F))`.
|
|
||||||
- `make_loose_fall(modifier)`: если НЕ solid (`tiles & 0x20 == 0`) и
|
|
||||||
`(sbyte)modif <= 0` → `modif = modifier`, `add_trob(type 0)`.
|
|
||||||
- `animate_loose`: `++modif`; когда `modif >= 11` (loose_floor_delay) →
|
|
||||||
`remove_loose()` (тайл → empty) + `add_mob()` (спавн падающего куска).
|
|
||||||
|
|
||||||
## 4. Падающий кусок (mob)
|
|
||||||
|
|
||||||
- `add_mob()` кладёт `curmob` в `mobs[]` (до 14). `do_mobs()` каждый
|
|
||||||
кадр: `move_mob()` (гравитация, y растёт) + `check_loose_fall_on_kid()`
|
|
||||||
(урон Киду/страже, если попал).
|
|
||||||
- Приземление куска → тайл под ним `curr_room_tiles[...] = tiles_14_debris`
|
|
||||||
(seg007 move_mob:1053). Т.е. **loose(11) упал → сверху empty(0), снизу
|
|
||||||
debris(14)**.
|
|
||||||
|
|
||||||
## 5. Отрисовка по статусу
|
|
||||||
|
|
||||||
- Куски тайла: `loose_fram_left[]={41,69,41,70,70,41,41,41,70,70,70,0}`,
|
|
||||||
`loose_fram_right[]={42,71,...}`, `loose_fram_bottom[]={43,73,...}`
|
|
||||||
(env-спрайты, seg008:518/596/608).
|
|
||||||
- Индекс кадра = `get_loose_frame(modifier)` (seg008): `0` = ровный
|
|
||||||
(41/42/43); `1..10` = дрожащие варианты (69–74); при бите7/большой
|
|
||||||
задержке — низкие индексы.
|
|
||||||
- **До падения**: рисуем loose с `get_loose_frame(modif)` (0 = ровно,
|
|
||||||
иначе колеблется). **После**: сверху empty, снизу debris(14) — обычная
|
|
||||||
статическая отрисовка (у нас уже есть tile 0x0E/14 debris в tile_table).
|
|
||||||
|
|
||||||
## 6. Что нужно в нашем движке (сейчас НЕТ)
|
|
||||||
|
|
||||||
Наш `pop_bg` рисует комнату СТАТИЧЕСКИ один раз. Loose-полы требуют
|
|
||||||
**динамического тайлового слоя**:
|
|
||||||
1. **Массив модификаторов** `room_modif[30]` (у нас есть `bg[30]` — можно
|
|
||||||
переиспользовать/рядом) — состояние каждого тайла.
|
|
||||||
2. **Очередь trob** (список анимируемых тайлов) + `animate_loose` пер-кадр
|
|
||||||
→ перерисовка ТОЛЬКО изменившихся тайлов (как heal-прямоугольник Kid).
|
|
||||||
3. **make_loose_fall / do_knock / loose_make_shake** — триггеры (из
|
|
||||||
физики Kid: приземление→knock, стойка на loose→fall).
|
|
||||||
4. **mob-система** (падающий кусок): минимум 1–2 mob'а, гравитация,
|
|
||||||
приземление → debris. Урон Киду (`check_loose_fall_on_kid`) — можно
|
|
||||||
Фазой 2.
|
|
||||||
5. **Перерисовка тайла**: `draw_tile(row,col)` у нас уже умеет loose
|
|
||||||
(`code==11`, `loose_fram_*` в env) — нужно вызывать его выборочно с
|
|
||||||
текущим модификатором (сейчас draw_tile берёт статический bg).
|
|
||||||
|
|
||||||
**Порядок реализации (предложение):**
|
|
||||||
- L1: room_modif[] + выборочная перерисовка тайла по модификатору
|
|
||||||
(draw_loose с get_loose_frame) — статика→динамика одного тайла.
|
|
||||||
- L2: trob-очередь + animate_loose (тряска по do_knock на приземлении).
|
|
||||||
- L3: make_loose_fall (стойка на loose) + отсчёт + remove → empty.
|
|
||||||
- L4: mob (падающий кусок → debris снизу).
|
|
||||||
- L5: урон Киду от падающего куска.
|
|
||||||
|
|
||||||
## Конкретика из SDLPoP (сверено 2026-07-18, готово к реализации)
|
|
||||||
|
|
||||||
Таблицы (seg008.c), индекс = `get_loose_frame(modif)`:
|
|
||||||
- `loose_fram_left[] = {41,69,41,70,70,41,41,41,70,70,70,0}`
|
|
||||||
- `loose_fram_right[] = {42,71,42,72,72,42,42,42,72,72,72,0}`
|
|
||||||
- `loose_fram_bottom[]= {43,73,43,74,74,43,43,43,74,74,74,0}`
|
|
||||||
- `get_loose_frame(m)`: если `(m&0x80)` (или delay>11) → `m&=0x7F; if(m>10) return 1;` → `return m;`
|
|
||||||
- `y_loose_land[] = {2,65,128,191,254}` (mob), `loose_floor_delay = 11`.
|
|
||||||
|
|
||||||
Триггеры (call-sites):
|
|
||||||
- **make_loose_fall(modifier=1)** — из `check_press()` (seg006): когда Kid
|
|
||||||
СТОИТ на тайле (FRAME_NEEDS_FLOOR, action < hang_climb / turn / bumped) и
|
|
||||||
`get_tile_at_char()==11`; ИЛИ `frame==79` (прыжок вверх) и
|
|
||||||
`get_tile_above_char()==11` (пробой сверху). `tile_is_floor(11)==1` —
|
|
||||||
Kid стоит на loose (start_fall НЕ зовётся). Тело:
|
|
||||||
`if(!(tile&0x20) && (sbyte)modif<=0){ modif=modifier; add_trob(type0); }`
|
|
||||||
- **do_knock(row)** — на ЖЁСТКОМ приземлении (SEQ_KNOCK_DOWN→check_knock,
|
|
||||||
seg003): по всем колонкам ряда `if(tile==11) loose_make_shake()`
|
|
||||||
(`if(modif==0){ modif=0x80; add_trob(type1); }`).
|
|
||||||
- **animate_loose** (каждый кадр, seg007:816): `++modif`; если `&0x80`
|
|
||||||
(тряска): `if(modif>=0x84){modif=0; trob=-1;}`; иначе (отсчёт):
|
|
||||||
`if(modif>=11){ remove_loose(tile→0); trob=-1; add_mob(); } else shake`.
|
|
||||||
|
|
||||||
## Интеграция в наш движок (roomtest) — подход
|
|
||||||
|
|
||||||
Наш движок ПЕКЁТ комнату один раз (двойной буфер: своя ОЗУ-копия на
|
|
||||||
страницу). Loose требует динамики + общего состояния pop_map↔pop_bg:
|
|
||||||
1. **Общая МУТАБЕЛЬНАЯ копия комнаты**: roomtest.c держит `uint8_t
|
|
||||||
room_fg[30]` (копия room1_fg) и передаёт ОДИН указатель и в
|
|
||||||
`pop_room_draw`, и в `pop_map_set` → мутации loose видны обоим.
|
|
||||||
(Сейчас g_fg — `const`; сделать неконстантным.)
|
|
||||||
2. **Состояние**: `uint8_t pop_loose_modif[30]` (0 / 0x80.. / 1..11).
|
|
||||||
3. **Модель** (pop_map): `check_press()` в `pop_phys_tick` (make_loose_fall
|
|
||||||
при стоянии на 11), `animate` каждый кадр.
|
|
||||||
4. **Перерисовка** (pop_bg, на back-странице каждый кадр):
|
|
||||||
- тряска/отсчёт (код всё ещё 11): `gfx_heal(tile rect)` (вернуть
|
|
||||||
печёный фон) + нарисовать loose-кадр (left/right/bottom по
|
|
||||||
get_loose_frame) банком SPRITE;
|
|
||||||
- падение (11→0): mutate `g_fg[pos]=0` + «запечь пустоту» на ОБЕИХ
|
|
||||||
страницах (bake-счётчик 2: чёрный bar + draw_tile empty банком NORMAL
|
|
||||||
на текущей странице 2 кадра подряд) → дальше heal показывает пусто.
|
|
||||||
5. **L4 mob**: падающий кусок → debris(14) снизу (y_loose_land); пока
|
|
||||||
отложено — упавший loose = пусто. **L5** урон — позже.
|
|
||||||
|
|
||||||
Риск: перерисовка динамического тайла в двойном буфере (per-page heal +
|
|
||||||
bake) — единственное тонкое место; остальное — прямой порт логики выше.
|
|
||||||
|
|
||||||
## Связанные
|
|
||||||
|
|
||||||
Триггеры завязаны на физику Kid ([[pop_hang_state]] check_press/check_grab,
|
|
||||||
приземление land/SEQ_KNOCK_DOWN). Отрисовка — [[pop_fore_layer]] /
|
|
||||||
[[pop_background_strategy]] (draw_tile уже знает loose_fram_*).
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Генераторы псевдослучайных чисел: запасные варианты
|
||||||
|
|
||||||
|
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально
|
||||||
|
можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра —
|
||||||
|
**сейчас менять ничего не нужно**.
|
||||||
|
|
||||||
|
## Что стоит сейчас
|
||||||
|
|
||||||
|
`pop_geom.c`, ветка `POP_PRANDOM_EXACT=1` (по умолчанию) — LCG оригинала
|
||||||
|
`s = s*214013 + 2531011`, шаг написан на Z80-ассемблере (единственное такое
|
||||||
|
место в порте). Схема Горнера по разреженной записи константы:
|
||||||
|
|
||||||
|
```
|
||||||
|
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
|
||||||
|
```
|
||||||
|
|
||||||
|
17 удвоений, три сложения, одно вычитание; величина `3*s`, нужная в конце,
|
||||||
|
попадается по дороге на втором шаге. Тело — **≈1 020 тактов** по статическому
|
||||||
|
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
|
||||||
|
с эталоном.
|
||||||
|
|
||||||
|
Вторая ветка, `POP_PRANDOM_EXACT=0` — xorshift16 + шаг Вейля на C.
|
||||||
|
Совместимость теряется.
|
||||||
|
|
||||||
|
Замер в MAME, комната 3, 175 кадров (медиана кадра):
|
||||||
|
|
||||||
|
| вариант | кадр | prandom → torch_draw |
|
||||||
|
|---|---|---|
|
||||||
|
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
|
||||||
|
| C, xorshift16 + Вейль | 397 986 | 7 927 |
|
||||||
|
| **asm, бит-в-бит (сейчас)** | **400 800** | **9 331** |
|
||||||
|
|
||||||
|
## Вариант A — комбинированный LFSR + LCG, ~148 тактов
|
||||||
|
|
||||||
|
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
|
||||||
|
|
||||||
|
```z80
|
||||||
|
prng16:
|
||||||
|
seed1=$+1
|
||||||
|
ld hl, 9999
|
||||||
|
ld b, h
|
||||||
|
ld c, l
|
||||||
|
add hl, hl
|
||||||
|
add hl, hl
|
||||||
|
inc l
|
||||||
|
add hl, bc
|
||||||
|
ld (seed1), hl
|
||||||
|
seed2=$+1
|
||||||
|
ld hl, 987
|
||||||
|
add hl, hl
|
||||||
|
sbc a, a
|
||||||
|
and 101101b
|
||||||
|
xor l
|
||||||
|
ld l, a
|
||||||
|
ld (seed2), hl
|
||||||
|
add hl, bc
|
||||||
|
ret
|
||||||
|
```
|
||||||
|
|
||||||
|
Устройство: `seed1` — LCG `x = 5x + 1` (по модулю 2^16; `inc l` вместо
|
||||||
|
`inc hl` — экономия байта, на период не влияет). `seed2` — 16-битный
|
||||||
|
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
|
||||||
|
`0x2D` (примитивный многочлен `x^16 + x^5 + x^3 + x^2 + 1`). На выходе
|
||||||
|
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
|
||||||
|
|
||||||
|
**Что мешает взять как есть:** сиды зашиты в код (SMC), а нам нужны ДВЕ
|
||||||
|
независимые последовательности — раскладка кладки и анимация тайлов.
|
||||||
|
Пришлось бы передавать состояние через указатель, как сейчас у `pop_prandom`
|
||||||
|
(это +20…40 тактов, не принципиально).
|
||||||
|
|
||||||
|
## Вариант B — xorshift(7,9,8), ~86 тактов
|
||||||
|
|
||||||
|
Самый быстрый, период 65535.
|
||||||
|
|
||||||
|
```z80
|
||||||
|
xrnd:
|
||||||
|
ld hl, 1 ; seed must not be 0
|
||||||
|
ld a, h
|
||||||
|
rra
|
||||||
|
ld a, l
|
||||||
|
rra
|
||||||
|
xor h
|
||||||
|
ld h, a
|
||||||
|
ld a, l
|
||||||
|
rra
|
||||||
|
ld a, h
|
||||||
|
rra
|
||||||
|
xor l
|
||||||
|
ld l, a
|
||||||
|
xor h
|
||||||
|
ld h, a
|
||||||
|
ld (xrnd+1), hl
|
||||||
|
ret
|
||||||
|
```
|
||||||
|
|
||||||
|
**Две оговорки.** Ноль — неподвижная точка, а сид раскладки кладки у нас
|
||||||
|
считается как `номер комнаты + смещение ряда + колонка` и вполне может
|
||||||
|
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
|
||||||
|
что в варианте A.
|
||||||
|
|
||||||
|
## Чего НЕ брать: RND из Apple II
|
||||||
|
|
||||||
|
Оригинальный `Prince-of-Persia-Apple-II`:
|
||||||
|
|
||||||
|
```
|
||||||
|
RNDseed := (5 * RNDseed + 23) mod 256
|
||||||
|
```
|
||||||
|
|
||||||
|
```asm
|
||||||
|
RND
|
||||||
|
lda RNDseed
|
||||||
|
asl
|
||||||
|
asl
|
||||||
|
clc
|
||||||
|
adc RNDseed
|
||||||
|
clc
|
||||||
|
adc #23
|
||||||
|
sta RNDseed
|
||||||
|
rts
|
||||||
|
```
|
||||||
|
|
||||||
|
Полный период 256 (`a ≡ 1 mod 4`, `c` нечётное), и для своего движка он
|
||||||
|
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
|
||||||
|
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
|
||||||
|
`prandom(1)` (ОДИН бит) и `prandom(4)`, то есть вместо шума получилась бы
|
||||||
|
аккуратная шахматка.
|
||||||
|
|
||||||
|
## Сколько реально можно выиграть
|
||||||
|
|
||||||
|
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
|
||||||
|
весь расход: остаются обёртка `pop_prandom`, приведение к диапазону
|
||||||
|
`pop_rnd_fit` и ABI вызова. Верхняя граница выигрыша видна из замера выше:
|
||||||
|
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
|
||||||
|
**2 814 тактов за кадр (0.65 %)** при двух вызовах за кадр, и это ПОТОЛОК —
|
||||||
|
любой из вариантов A/B ниже него не опустится.
|
||||||
|
|
||||||
|
Порядок действий, если понадобится:
|
||||||
|
|
||||||
|
1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы
|
||||||
|
на вызов приходился один `call`, а не три. Это ничего не ломает и не
|
||||||
|
трогает совместимость с эталоном.
|
||||||
|
2. И только если этого мало — менять генератор, начиная с варианта A
|
||||||
|
(качество последовательности у него не хуже LCG, в отличие от B).
|
||||||
|
|
||||||
|
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
|
||||||
|
(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают
|
||||||
|
`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и
|
||||||
|
цена вопроса вырастет во столько же раз.
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
|
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
|
||||||
|
|
||||||
|
> **Статус: ЖИВОЙ ПЛАН, сделан частично (сверено 2026-08-01).**
|
||||||
|
> - **S1 — сделан:** `kid_room` заведён отдельно от `cur_room`,
|
||||||
|
> `update_kid_render_dx()` (`roomtest.c`) даёт рендер-смещение ∓140, а
|
||||||
|
> `pop_kid_set_render_dx` применяет его в отрисовке. Фактически это пока
|
||||||
|
> каркас: `enter_room` держит `kid_room == cur_room`, так что смещение
|
||||||
|
> всегда 0.
|
||||||
|
> - **S2/S3/S4 — не сделаны и не срочны.** Исходный повод (баг #4,
|
||||||
|
> пинг-понг у шва) закрыт иначе — поправкой odd-pixel в
|
||||||
|
> `char_x_forward_edge` + `pop_leave_timer` (разбор корня —
|
||||||
|
> `../roomtest/bug_closed.md`, BUG-SEAM-PINGPONG).
|
||||||
|
>
|
||||||
|
> **Зачем документ остаётся.** Полная straddle-модель понадобится для:
|
||||||
|
> (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4),
|
||||||
|
> (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у
|
||||||
|
> шва (S4). Брать из `../roomtest/TASKS.md`, когда дойдёт очередь.
|
||||||
|
|
||||||
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
|
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
|
||||||
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
|
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
|
||||||
|
|
||||||
@@ -62,7 +78,9 @@ drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol`
|
|||||||
### S4. Полировка
|
### S4. Полировка
|
||||||
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
|
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
|
||||||
|
|
||||||
## Связанные баги (bug_list.md)
|
## Связанные баги — все ЗАКРЫТЫ (`../roomtest/bug_closed.md`)
|
||||||
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
|
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
|
||||||
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
|
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
|
||||||
колонны). Memory: `pop_seam_room_model`.
|
колонны) — починены без полной straddle-модели. То есть S4 «полировка
|
||||||
|
окклюзии» осталась актуальной только для окклюзии У ШВА при straddle.
|
||||||
|
Memory: `pop_seam_room_model`.
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
# roomtest — план оптимизации по размеру + переход на huge/banking
|
|
||||||
|
|
||||||
Статус: **план для отдельной сессии** (2026-07-21). Документ самодостаточный
|
|
||||||
(рассчитан на старт с пустого контекста). Цель — освободить место: сейчас
|
|
||||||
`applications/PoP/roomtest` в режиме `small` почти упёрся в потолок 32 КБ.
|
|
||||||
|
|
||||||
Правило проекта (`applications/PoP/CLAUDE.md`): механику/раскладку памяти
|
|
||||||
сверять с исходником и с memory (`sprinter_memory_modes`, `sdcc_banking`,
|
|
||||||
`bank_local_data_pattern`, `pop_banking_architecture`). Перед оптимизацией —
|
|
||||||
`make size-check`-подобный замер до/после (здесь — руками по `.map`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Как мерить
|
|
||||||
|
|
||||||
- Сборка: `cd applications/PoP/roomtest && make roomtest.exe` (режим `small`,
|
|
||||||
`--gfx 256`). Карта символов — `.sprinter-cc-roomtest/roomtest.map`
|
|
||||||
(адреса сдвигаются при каждой пересборке!).
|
|
||||||
- Размеры областей — из `.map` (`_CODE`, `_DATA`, `_BSS`).
|
|
||||||
- Вклад модулей в `_CODE` — атрибуция диапазонов между символами по модулю
|
|
||||||
(скрипт-однострочник на python в истории; группировать символы `.map` по
|
|
||||||
3-й колонке-модулю и суммировать `addr[i+1]-addr[i]`).
|
|
||||||
- MAME-проверка после изменений раскладки ОБЯЗАТЕЛЬНА (режимы памяти —
|
|
||||||
типовой источник «молча ломается», см. `sprinter_memory_modes`).
|
|
||||||
|
|
||||||
## 1. ТЕКУЩЕЕ СОСТОЯНИЕ (замер 2026-07-21)
|
|
||||||
|
|
||||||
Режим `small` = единое пространство **W1+W2 = 0x4000..0xBFFF (32 КБ)**; CODE с
|
|
||||||
0x4100, DATA/BSS/heap цепляются ЗА CODE автоматически (`--data-loc 0` =
|
|
||||||
linker chains), стек — вверху W2.
|
|
||||||
|
|
||||||
| Область | Размер | Диапазон |
|
|
||||||
|---------|--------|----------|
|
|
||||||
| `_CODE` | ~27 250 Б (0x6A6F) | 0x4100–0xAB6F |
|
|
||||||
| `_HOME` | 227 Б | 0xAB6F |
|
|
||||||
| `_DATA` | 3 449 Б (0x0D79) | 0xAC78–0xB9F1 |
|
|
||||||
| `_BSS` | 290 Б | |
|
|
||||||
|
|
||||||
**Образ ≈ 31.2 КБ; до верха W2 (0xBFFF) остаётся ≈ 1.3 КБ на кучу+стек.**
|
|
||||||
Куча в roomtest почти не используется (атласы/уровень — в EMM-страницах),
|
|
||||||
но запас критично мал.
|
|
||||||
|
|
||||||
### Вклад модулей в _CODE (по .map, приблизительно)
|
|
||||||
```
|
|
||||||
7003 pop_bg (вся отрисовка тайлов/слоёв/wall_pattern)
|
|
||||||
6326 pop_kid (из них ~3745 Б — СТАТ. ТАБЛИЦЫ kid_data.h, см. ниже)
|
|
||||||
3762 pop_map (коллизия/физика/пики)
|
|
||||||
1455 pop_trob (кнопки/ворота/пики-каркас)
|
|
||||||
1224 pop_level (загрузка уровня, doorlink)
|
|
||||||
914 roomtest (главный цикл)
|
|
||||||
~7000 libc/libbgi (gfx_blit*, atlas_load, kbd_raw, open/read, irq, div/mul…)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Крупные СТАТИЧЕСКИЕ данные (сейчас в _CODE как `const`)
|
|
||||||
- **`kid_data.h` — самый большой кусок, ~3.7 КБ**, живёт в _CODE (атрибутируется
|
|
||||||
pop_kid):
|
|
||||||
- `kid_seqtbl[2310]` — байткод последовательностей (play_seq).
|
|
||||||
- `kid_frames[241]` × 5 Б = 1205 Б — таблица кадров (image,dx,dy,flags,sword).
|
|
||||||
- `kid_seq_off[115]` × 2 Б = 230 Б — смещения seq.
|
|
||||||
- `pop_bg`: `tile_table[31]`×12 = 372 Б + ~20 мелких const-таблиц (COL_XH,
|
|
||||||
WALL_FRAM_*, SPIKES_FRAM_{RIGHT,LEFT,FORE}, LOOSE_FRAM_*, DOOR_FRAM_SLICE,
|
|
||||||
BLUELINE_*, LPOS/RPOS, FLOOR_LEFT_OVERLAY) — суммарно ~0.5–0.7 КБ.
|
|
||||||
- `pop_map`: `x_bump[20]`, `y_land[5]`, `wall_dl/dr`, `dir_front/behind` — ~100 Б.
|
|
||||||
- В `_DATA` (W2, не CODE): `room_modif[24][30]`=720 Б + копии LINKLOC/LINKMAP=512 Б
|
|
||||||
(pop_trob/pop_level) + рабочие массивы roomtest.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. ПУТЬ A — оптимизация КОДА (без смены модели)
|
|
||||||
|
|
||||||
1. **Компиляторные флаги** (`bin/sprinter-cc`): попробовать `--opt-code-size`
|
|
||||||
у SDCC и подобрать `--max-allocs` (сейчас дефолт 100000; меньше = мельче код,
|
|
||||||
но медленнее компиляция; см. `mdview2_size_budget` — там `--max-allocs`
|
|
||||||
давал −1.4 КБ). Замерить каждый модуль отдельно.
|
|
||||||
2. **Дедуп подстановки нажатой кнопки**: логика `opener→floor / closer→stuck`
|
|
||||||
по таймеру связи ПРОДУБЛИРОВАНА в `draw_tile` и `fore_tile` (pop_bg.c).
|
|
||||||
Вынести в `static inline`/helper `subst_pressed_button(code,mod)`.
|
|
||||||
3. **wall_pattern / prandom** (pop_bg): 32-битный LCG (`unsigned long`) —
|
|
||||||
пользователь не любит 32-бит (см. `avoid_32bit_arith_z80`); но это PRNG
|
|
||||||
оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только
|
|
||||||
если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность.
|
|
||||||
4. **Ревизия дублей**: `y_to_row` определён в pop_bg И pop_map; мелкие
|
|
||||||
геометрические хелперы дублируются — свести в один internal-модуль.
|
|
||||||
5. `/simplify`-проход по последним правкам Фазы B (pop_trob/pop_bg).
|
|
||||||
|
|
||||||
Ожидаемый выигрыш пути A: единицы–первые сотни байт на пункт; в сумме,
|
|
||||||
оптимистично, ~1–2 КБ. Недостаточно как единственная мера.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ПУТЬ B — вынос СТАТ. ДАННЫХ в EMM-страницы (с атласами / с level)
|
|
||||||
|
|
||||||
**Идея (по замечанию пользователя):** EMM-страницы атласов и уровня
|
|
||||||
использованы лишь частично (страница 16 КБ, данных меньше), в «хвосте» —
|
|
||||||
свободное место. Часть `const`-таблиц можно хранить ТАМ, а не в _CODE/_DATA,
|
|
||||||
если таблица читается ИМЕННО ТОГДА, когда нужная страница уже в W0.
|
|
||||||
|
|
||||||
**Механика W0:** атласы блитятся из W0 (`_gfx_w0_state`: `_gfx_w0_cur` —
|
|
||||||
спрайт-страница в W0; ISR-стаб `_gfx_w0_isr` возвращает её после прерывания).
|
|
||||||
Уровень (pop_level) маппит свою страницу в W0 на время извлечения
|
|
||||||
(`gfx_w0_map`/`gfx_w0_unmap`). → пока страница в W0, CPU может читать и
|
|
||||||
данные из неё по адресам 0x0000..0x3FFF.
|
|
||||||
|
|
||||||
**Категоризация таблиц по W0-контексту (задача сессии — уточнить по каждой):**
|
|
||||||
|
|
||||||
- **(a) Читается, когда в W0 АТЛАС** → хранить в свободном хвосте атлас-страницы.
|
|
||||||
Кандидаты — таблицы, которые нужны В МОМЕНТ блита конкретного атласа.
|
|
||||||
ГРАБЛИ: `draw_tile` читает `tile_table`/`COL_XH` ДО блита (чтобы решить, какой
|
|
||||||
спрайт/куда) — в этот момент в W0 может быть ДРУГАЯ страница (DSS/предыдущий
|
|
||||||
атлас). Т.е. большинство draw-таблиц читаются ВНЕ W0-атлас-контекста →
|
|
||||||
«в лоб» не переносятся. Нужен аудит КАЖДОГО чтения: гарантирована ли нужная
|
|
||||||
страница в W0 в этот тик.
|
|
||||||
- **(b) Читается, когда в W0 LEVEL** → хранить с уровнем (в его странице; там
|
|
||||||
~13.7 КБ свободно из 16). Кандидаты: константы декода doorlink, разбор
|
|
||||||
комнат — всё, что pop_level делает под `gfx_w0_map(lvl_page)`.
|
|
||||||
- **(c) Нужна и там, и там** → дублировать в обеих страницах ЛИБО оставить
|
|
||||||
резидентной (если дубли дороже экономии).
|
|
||||||
- **(d) Читается в чистой ЛОГИКЕ (W0 не важен)** → перенос требует ЯВНОГО
|
|
||||||
`gfx_w0_map` на каждое чтение (дорого, особенно в горячих циклах) → как
|
|
||||||
правило оставить резидентной.
|
|
||||||
|
|
||||||
**Отдельно `kid_data.h` (3.7 КБ — самый жирный кандидат):**
|
|
||||||
- `kid_frames`/`kid_seqtbl` читаются в `play_seq` (ЧИСТАЯ логика, каждый тик) И
|
|
||||||
в `kid_draw` (блит из kid-атласа, kid-страница в W0). Т.е. частично (a),
|
|
||||||
частично (d). Перенос всей таблицы в kid-атлас-страницу заставит `play_seq`
|
|
||||||
делать `gfx_w0_map` на каждый шаг байткода → замерить стоимость (может убить
|
|
||||||
бюджет спрайтов, см. `sprite_engine_perf`). Вариант: держать в EMM отдельной
|
|
||||||
страницей данных Kid и маппить один раз на кадр вокруг kid_tick+kid_draw.
|
|
||||||
- Это самый большой одиночный выигрыш (−3.7 КБ из _CODE), но и самый рискованный
|
|
||||||
по скорости — приоритетный к ПРОТОТИПИРОВАНИЮ и замеру.
|
|
||||||
|
|
||||||
**Паттерн переноса writable/const данных в банк/страницу:** см. memory
|
|
||||||
`bank_local_data_pattern` (--codeseg/--constseg/--dataseg BANKn + trampoline-fix
|
|
||||||
+ mkexe -p 0) и `sdcc_static_storage_gotcha`.
|
|
||||||
|
|
||||||
### 3.1 Свободное место в страницах (замер 2026-07-21, страница = 16384 Б)
|
|
||||||
```
|
|
||||||
BG-атласы: размер свободно
|
|
||||||
pop_env0.atl 10578 5806
|
|
||||||
pop_env1.atl 12449 3935 <- САМАЯ ТЕСНАЯ из bg
|
|
||||||
pop_env2.atl 10798 5586
|
|
||||||
pop_env3.atl 5032 11352 <- много места
|
|
||||||
pop_env4.atl 8498 7886
|
|
||||||
pop_wall.atl 11543 4841
|
|
||||||
pop_fore.atl 7763 8621
|
|
||||||
Kid-атласы (28 стр): free min=6161 max=15452 avg=9722
|
|
||||||
Level (res2001.bin): данные 2305, свободно ~13823 (16384 − 0x100 стаб − 2305)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Выводы по вместимости:**
|
|
||||||
- **Макс. данных в ОДНОМ атлас-банке = свободный хвост ЭТОЙ страницы** (см.
|
|
||||||
таблицу). Связывающее ограничение — самая тесная нужная страница (env1 =
|
|
||||||
3935 Б; не перегружать её).
|
|
||||||
- Если страница будет маппиться в **W0** — минус ~0x100 Б на ISR-стаб (как
|
|
||||||
level). Атлас-страницы стаб УЖЕ содержат (atlas_load патчит) → данные класть
|
|
||||||
в хвост ПОСЛЕ атласа.
|
|
||||||
- **`kid_data.h` (3.7 КБ) влезает в kid-страницу** (min free 6161) или в
|
|
||||||
отдельную выделенную страницу данных Kid — предпочтительно отдельную (маппить
|
|
||||||
раз на кадр, не конфликтуя с kid-атласами блита).
|
|
||||||
- **Level-таблицы** — вагон места в level-странице (~13.8 КБ).
|
|
||||||
- **BG draw-таблицы** (~0.7 КБ) влезут в env3/fore/env4 (много free), НО см.
|
|
||||||
граблю W0-контекста в §3(a) — читаются ли они, когда нужная страница в W0.
|
|
||||||
- **Выделенная страница ТОЛЬКО под данные** (не делить с атласом) = до ~16 КБ
|
|
||||||
(−0x100 стаб при W0-маппинге). EMM-бюджет это позволяет (см.
|
|
||||||
`sprinter_emm_budget`: 215/3440 КБ free на старте).
|
|
||||||
- **Принудительно уменьшать макс. атлас (репак мельче) — КРАЙНИЙ случай:** это
|
|
||||||
резко поднимет число атлас-банков (сейчас 5 env-страниц адресуются как id>>5;
|
|
||||||
дробление ломает эту адресацию и множит страницы). Сначала использовать
|
|
||||||
СУЩЕСТВУЮЩИЙ свободный хвост и отдельные data-страницы.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ПУТЬ C — переход на huge (banked code)
|
|
||||||
|
|
||||||
### 4.1 Что такое huge сейчас (`bin/sprinter-cc`, `runtime/crt0_banked`)
|
|
||||||
- `--memory huge`: `MODE_CODE_LOC=0x4100`, **`MODE_DATA_LOC=0x8000` (ФИКС.)**,
|
|
||||||
banked code в W3. crt0_banked, как crt0_small, авто-детектит W2.
|
|
||||||
Помечено `[TODO]` — не обкатано.
|
|
||||||
- Отличие от small: small цепляет DATA сразу за CODE (`--data-loc 0`); huge
|
|
||||||
ФИКСИРУЕТ DATA на 0x8000.
|
|
||||||
|
|
||||||
### 4.2 ТРЕБОВАНИЕ (по пользователю): huge должен переносить DATA динамически
|
|
||||||
Сейчас huge жёстко кладёт DATA на 0x8000. Если РЕЗИДЕНТНЫЙ CODE вылезет за
|
|
||||||
0x8000 (W1 = только 0x4000..0x7FFF ≈ 16 КБ; резидент > 16 КБ лезет в W2) →
|
|
||||||
коллизия с DATA. **Надо научить huge класть DATA динамически ЗА резидентным
|
|
||||||
CODE (как small: `--data-loc 0` + crt0 считает старт), а не на фикс 0x8000.**
|
|
||||||
Тогда huge = «small-раскладка резидента (W1+W2, DATA за CODE) + ДОП. код в
|
|
||||||
банках W3». Это первый пункт работ по huge.
|
|
||||||
|
|
||||||
### 4.3 КОНФЛИКТ: графика тоже хочет W3 (ключевой риск)
|
|
||||||
`pop_banking_architecture` прямо говорит: **графику нельзя в W3** (блиты/атласы
|
|
||||||
используют окна; см. §4.5). Поэтому в банки W3 можно выносить ТОЛЬКО
|
|
||||||
НЕ-графические блоки, и такой банк НЕ должен во время своего исполнения держать
|
|
||||||
графику в W3. Если W3-банкованная функция ЗОВЁТ графику (которой нужен W3),
|
|
||||||
трамплин обязан сохранить/восстановить банк вокруг вызова (проверить, что
|
|
||||||
banking-ABI это делает — `sdcc_banking`). Альтернатива без этого риска —
|
|
||||||
**big + BANK_W1** (банк кода в W1, не W3), рекомендованная в
|
|
||||||
`pop_banking_architecture` именно из-за W3-графики. Сессия должна выбрать:
|
|
||||||
huge(W3) с аккуратным save/restore ИЛИ big(BANK_W1).
|
|
||||||
|
|
||||||
### 4.4 Какие блоки МОЖНО вынести (не работают с графикой напрямую)
|
|
||||||
Замер graphics-ref по модулям (grep `gfx_|blit|env_b|wall_b|fore_b|setfillstyle|
|
|
||||||
bar(|GFX_BANK|initgraph`):
|
|
||||||
```
|
|
||||||
pop_bg.c : 83 — РЕЗИДЕНТ (вся отрисовка)
|
|
||||||
roomtest.c : 23 — РЕЗИДЕНТ (главный цикл + флип страниц)
|
|
||||||
pop_level.c : 17 — использует gfx_w0_map (W0, не W3-блиты) — ПОГРАНИЧНЫЙ
|
|
||||||
pop_kid.c : 12 — kid_draw = графика; НО play_seq — чистая логика (можно split)
|
|
||||||
pop_ctrl.c : 0 — КАНДИДАТ В БАНК (ввод/диспетчер control)
|
|
||||||
pop_map.c : 0 — КАНДИДАТ В БАНК (коллизия/физика, ~3.8 КБ) — лучший по объёму
|
|
||||||
pop_trob.c : 0 — КАНДИДАТ В БАНК (кнопки/ворота/пики-логика)
|
|
||||||
```
|
|
||||||
- **Лучшие кандидаты в W3-банк(и): pop_map + pop_trob + pop_ctrl** (нет прямой
|
|
||||||
графики; вместе ~5.3 КБ CODE). Освобождают резидент → он влезает в W1.
|
|
||||||
- **Осторожно с межбанковыми вызовами:** pop_map/pop_trob ЗОВУТ pop_bg
|
|
||||||
(перерисовка loose/пик/кнопок/шва) и pop_kid (play_seq/kid_set_seq). Это
|
|
||||||
кросс-банк вызовы через трамплин (`sdcc_banking`: стек +3 байта, виртуальный
|
|
||||||
24-битный адрес). Правило `pop_banking_architecture`: «один файл = один банк
|
|
||||||
= прямые вызовы», main резидентен. Проверить, что трамплин сохраняет W3
|
|
||||||
вокруг вызова в графический pop_bg (см. §4.3).
|
|
||||||
- **pop_kid split** (по желанию): вынести play_seq/seqtbl-интерпретатор
|
|
||||||
(логика + таблицы kid_data.h) в банк, оставить kid_draw/kid_heal резидентными.
|
|
||||||
Даёт и −код, и −данные из резидента, но требует аккуратного разделения TU
|
|
||||||
(1 функция = 1 модуль, см. `libc_one_function_per_module`).
|
|
||||||
- **pop_level: пограничный** — не блитит, но маппит уровень в W0; банковать
|
|
||||||
можно, если W0-логика совместима с трамплином (проверить ISR-стаб взаимодействие).
|
|
||||||
|
|
||||||
### 4.5 Почему графику нельзя в W3 (контекст)
|
|
||||||
Блиттер держит спрайт-страницу атласа в **W0** (`_gfx_w0_state`,
|
|
||||||
`_gfx_w0_isr`). Ускоритель/адресация видео — отдельная тема (см.
|
|
||||||
`sprinter_accelerator`, `sprinter_graphics`). W3 в banked-раскладке — окно
|
|
||||||
кода-банка; смешивать с окном, которое графика перемапливает, нельзя без
|
|
||||||
save/restore. Детально — `pop_banking_architecture`, `graphics_constraints`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии)
|
|
||||||
|
|
||||||
1. **Замер-базлайн** (CODE/DATA/BSS + per-module) — зафиксировать до.
|
|
||||||
2. **Путь A** дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый −1..2 КБ.
|
|
||||||
3. **huge §4.2**: научить huge класть DATA динамически (как small) — инфраструктурный
|
|
||||||
пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем
|
|
||||||
резиденте (пока без выноса — просто huge-раскладка = small + пустой W3).
|
|
||||||
4. **huge §4.4**: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить
|
|
||||||
кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1.
|
|
||||||
5. **Путь B** (по остатку нужды): прототип выноса `kid_data.h` в EMM-страницу
|
|
||||||
Kid с маппингом раз на кадр; замерить скорость (`sprite_engine_perf`).
|
|
||||||
Затем аудит draw-таблиц по W0-контексту (§3 a/b/c/d).
|
|
||||||
|
|
||||||
## 6. Ссылки
|
|
||||||
- `bin/sprinter-cc` (§162+ — резолв memory-mode → CODE_LOC/DATA_LOC).
|
|
||||||
- `runtime/crt0_small.*`, `runtime/crt0_banked.*`, `runtime/bank.s`.
|
|
||||||
- memory: `sprinter_memory_modes`, `memory_modes_implemented`,
|
|
||||||
`setwin2_for_w2_alloc`, `sdcc_banking`, `bank_local_data_pattern`,
|
|
||||||
`pop_banking_architecture`, `avoid_32bit_arith_z80`,
|
|
||||||
`libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`.
|
|
||||||
- `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют
|
|
||||||
оптимизацию, но держать в уме при рефакторе pop_map/pop_bg).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Лишние блиты в горячем пути (добавлено 2026-07-27)
|
|
||||||
|
|
||||||
Найдено при разборе окклюзии по эталону SDLPoP: **наш «передний слой» рисовал
|
|
||||||
спрайты, которых в оригинале там нет** — это и артефакты, и лишняя работа
|
|
||||||
каждый кадр. Исправлено: `fore_tile` (вызывается для КАЖДОГО тайла футпринта
|
|
||||||
Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — переднюю кромку пола; в
|
|
||||||
оригинале `draw_tile_fore` (seg008:690) добавляет только `add_foretable`-часть,
|
|
||||||
а `bottom` идёт через `draw_tile_bottom` в backtable (ПОД персонажем).
|
|
||||||
Итог: −2..4 блита за кадр, `_CODE` −388 Б, ушла «тень» у основания колонны.
|
|
||||||
|
|
||||||
**Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли
|
|
||||||
этот спрайт в оригинале в ЭТОЙ таблице?):**
|
|
||||||
|
|
||||||
1. `pop_room_draw`/`draw_tile` — вызовы на входе в комнату не критичны по
|
|
||||||
скорости, но по ним стоит сверить состав слоёв (backtable vs foretable).
|
|
||||||
2. `overlay_mid_tile` — сейчас точный порт midtable-части `draw_tile2`;
|
|
||||||
проверить, не рисуем ли `base_id` там, где оригинал его не рисует
|
|
||||||
(loose: base=0, потому что кадр плиты идёт через `draw_loose` в backtable).
|
|
||||||
3. `pop_loose_mob_tick` — перерисовка соседнего тайла (`draw_tile(mob_row,
|
|
||||||
mob_col+1)`) КАЖДЫЙ кадр падения: в оригинале это `set_redraw_full` на
|
|
||||||
один кадр; можно ограничить только тайлом, который реально пересекается
|
|
||||||
с куском.
|
|
||||||
4. `pop_ceil_shake_draw` — heal 64×8 + два `draw_tile(-1,·)` на кадр тряски;
|
|
||||||
проверить, нужен ли второй тайл (правую грань loose в полосе потолка
|
|
||||||
оригинал не рисует вовсе — `draw_tile_aboveroom` без `draw_tile_anim_right`).
|
|
||||||
5. `fore_only_tile` для полосы потолка: вызывается для всех колонок габарита,
|
|
||||||
а оригинал (`redraw_needed_above`) — только для колонок с флагом
|
|
||||||
`redraw_frames_above`; сузить до колонок, реально задетых спрайтом.
|
|
||||||
6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить,
|
|
||||||
не зовём ли её там, где оригинал ограничивается `wall_fram_main`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Скорость отрисовки: замеры и запас (2026-07-27)
|
|
||||||
|
|
||||||
Профилирование в MAME (маркеры в порт 0xFE + `wpiset … totalcycles`, приём из
|
|
||||||
memory `mame_mcp_bridge`). Кадр Sprinter = **430 080 тактов**.
|
|
||||||
|
|
||||||
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
|
|
||||||
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
|
|
||||||
пересчёт src, нарезка полос >256), а не за пиксели:
|
|
||||||
|
|
||||||
| путь (спрайт 32×3) | тактов |
|
|
||||||
|---|---|
|
|
||||||
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
|
|
||||||
| линейное спрайтовое ядро без клипа (`putsprite` при `gfx_sprite_clip(0)`) | 4 617 |
|
|
||||||
|
|
||||||
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
|
|
||||||
кадра**; сам `bar` — только 13 308.
|
|
||||||
|
|
||||||
**СДЕЛАНО (шаг 1):** в libbgi добавлен `gfx_blit_noclip()`
|
|
||||||
(`common/gfx_blit_noclip.c`, прототип в `include/gfx.h`) — блит без клипа в
|
|
||||||
ТЕКУЩЕМ банке через линейное ядро; `pop_bg.blit_b` уходит на него, когда
|
|
||||||
спрайт целиком на экране и не нужен `g_clip_top`. Выигрыш ~2.9× на каждом
|
|
||||||
фоновом блите (подтверждено в MAME).
|
|
||||||
**ВАЖНО:** W3-скобку (`_bgi_begin/_bgi_end`) ставит САМА libbgi — вызывать её
|
|
||||||
из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
|
|
||||||
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
|
|
||||||
белый экран).
|
|
||||||
|
|
||||||
**ЗАПАС (шаг 2), когда перестанет хватать бюджета кадра:**
|
|
||||||
|
|
||||||
1. **Батчинг W3-скобки** — одна `_bgi_begin/_bgi_end` на весь `draw_tile`
|
|
||||||
вместо скобки на блит; нужен публичный batch-API в libbgi (как у
|
|
||||||
спрайтового движка). Осторожно: между begin/end стоит `DI` — длинная
|
|
||||||
серия задержит кадровое прерывание.
|
|
||||||
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
|
|
||||||
(`env 52`, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор
|
|
||||||
бара на высоту тайла) и выводить одним `gfx_blit_part` с обрезкой по фазе
|
|
||||||
`gate_bot_y & 7`: 7 блитов → 1.
|
|
||||||
3. **Не перерисовывать статичные части шва** — грань ворот (env 47, 26×62),
|
|
||||||
пол (41) и кромка (43) при анимации решётки не меняются; если стирать
|
|
||||||
только полосу баров, уйдут ещё 3 блита из 9.
|
|
||||||
4. См. также §7 (лишние блиты, которых нет в оригинале).
|
|
||||||
@@ -37,5 +37,5 @@ static const char *const kid_atl[KID_PAGES] = {
|
|||||||
};
|
};
|
||||||
#define KID_PAL_FILE "kid.pal"
|
#define KID_PAL_FILE "kid.pal"
|
||||||
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
|
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
|
||||||
#define KID_SWORD_ID0 20 /* индекс в атласе = sword_tbl.id - ID0 */
|
#define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -11,13 +11,23 @@
|
|||||||
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
|
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
|
||||||
по умолчанию баг у нас.
|
по умолчанию баг у нас.
|
||||||
|
|
||||||
|
**Что в работе сейчас — `TASKS.md`** (доска задач: приоритеты, критерии
|
||||||
|
готовности). Баги — `bug_list.md` (только ОТКРЫТЫЕ) и `bug_closed.md`
|
||||||
|
(закрытые + разбор корней: перед заведением нового бага грепни там по
|
||||||
|
симптому). План следующих уровней — `../docs/levels_plan.md`.
|
||||||
|
|
||||||
## Сборка и запуск
|
## Сборка и запуск
|
||||||
|
|
||||||
```
|
```
|
||||||
make # собрать roomtest.exe (упаковав ассеты через toolchain/)
|
make # собрать roomtest.exe (упаковав ассеты через toolchain/)
|
||||||
make run # exe + EXTRA_DATA на дискету + запуск MAME (см. корневой док)
|
make run # exe + EXTRA_DATA на дискету + запуск MAME (см. корневой док)
|
||||||
|
make -C tests-host # модульные тесты движка под ucsim_z80 (секунды, без MAME)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Логику, которую можно проверить без железа, покрывать в `tests-host/`
|
||||||
|
(обвязка — `testkit/`, там же почему прогон именно под z80). MAME остаётся
|
||||||
|
для отрисовки, банков, таймингов и клавиатуры.
|
||||||
|
|
||||||
`MEMORY=small`, `--gfx 256`. Ассеты (`pop_env0..4.atl`, `pop_wall.atl`,
|
`MEMORY=small`, `--gfx 256`. Ассеты (`pop_env0..4.atl`, `pop_wall.atl`,
|
||||||
`pop_fore.atl`, `kid0..27.atl`, `kid.pal`) генерируются python-скриптами
|
`pop_fore.atl`, `kid0..27.atl`, `kid.pal`) генерируются python-скриптами
|
||||||
`../toolchain/` — Makefile дёргает их сам при изменении. Данные комнаты —
|
`../toolchain/` — Makefile дёргает их сам при изменении. Данные комнаты —
|
||||||
@@ -34,7 +44,10 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
|
|||||||
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
|
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
|
||||||
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
|
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
|
||||||
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
|
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
|
||||||
**ESC** — выход.
|
**ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I** —
|
||||||
|
бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень,
|
||||||
|
**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру
|
||||||
|
(`pop_level_load_num`), на диске лежат все 15.
|
||||||
|
|
||||||
## Модули
|
## Модули
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,53 @@ PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
|||||||
EXAMPLE := roomtest
|
EXAMPLE := roomtest
|
||||||
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
||||||
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
||||||
MEMORY ?= small
|
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
||||||
EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c
|
# Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_gdraw
|
||||||
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c
|
# в одной странице — вызовы между ними прямые, если функция не помечена
|
||||||
|
# __banked). Так снято главное ограничение резидента: из банка его было
|
||||||
|
# не достать ни прямо, ни транзитивно, а банк→банк работает (трамплин
|
||||||
|
# сохраняет страницу окна на СТЕКЕ, проверено вложенной цепочкой
|
||||||
|
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
|
||||||
|
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
|
||||||
|
# sdcc_banked_call_rules.
|
||||||
|
# Банк 5 = pop_ctrl.c (MEM-BANK5). CODE и DATA делят одно 32-КБ
|
||||||
|
# пространство W1+W2, поэтому килобайт кода, уехавший в банк, — это
|
||||||
|
# килобайт, доступный данным. Кандидат выбран не по размеру, а по частоте
|
||||||
|
# вызова: диспетчер управления дёргается один раз за кадр на персонажа и
|
||||||
|
# горячих банк→банк переходов не создаёт (в отличие от pop_level, чей
|
||||||
|
# pop_level_tile зовётся из банка 2 на КАЖДЫЙ тайл). Шина control_*
|
||||||
|
# переехала в pop_state.c — см. там.
|
||||||
|
# --max-allocs-per-node: во сколько вариантов размещения регистров SDCC
|
||||||
|
# упирается на узел. У sprinter-cc дефолт 100000 (агрессивно, как fast-сборки
|
||||||
|
# библиотек) — на roomtest это МИНУТЫ на банк, потому что модули крупные.
|
||||||
|
# Дефолт SDCC — 3000, и для разработки его достаточно: разница в размере
|
||||||
|
# единицы процента, а сборка кратно быстрее. Замер (2026-08-05) — в шапке
|
||||||
|
# TASKS.md, запись BUILD-FAST.
|
||||||
|
#
|
||||||
|
# make — быстрая сборка (ALLOCS=3000)
|
||||||
|
# make ALLOCS=100000 — как раньше: минимальный код, для замеров размера
|
||||||
|
# и для «релизного» образа
|
||||||
|
#
|
||||||
|
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
|
||||||
|
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
|
||||||
|
ALLOCS ?= 3000
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS)
|
||||||
|
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
||||||
|
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||||
|
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||||
|
# MAME = время фазы в растровых строках.
|
||||||
|
PROF ?= 1
|
||||||
|
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||||
|
EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_level.c pop_redraw.c pop_geom.c pop_guard.c
|
||||||
|
|
||||||
BG_DIR := $(CURDIR)/../poc/res/bg
|
BG_DIR := $(CURDIR)/../poc/res/bg
|
||||||
KID_DIR := $(CURDIR)/../poc/res/kid
|
KID_DIR := $(CURDIR)/../poc/res/kid
|
||||||
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
|
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
|
||||||
LVL_DATA := $(LVL_DIR)/res2001.bin
|
# ВСЕ уровни игры (res2001..res2015, по 2305 Б = 34 КБ на всех): переход
|
||||||
|
# уровень→уровень и чит Shift+L требуют, чтобы файл следующего лежал на
|
||||||
|
# диске. res2000 — демо-уровень, в скоуп не входит.
|
||||||
|
LVL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
|
||||||
|
LVL_DATA := $(foreach n,$(LVL_NUMS),$(LVL_DIR)/res20$(n).bin)
|
||||||
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \
|
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \
|
||||||
$(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \
|
$(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \
|
||||||
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \
|
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \
|
||||||
@@ -22,16 +61,39 @@ BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.
|
|||||||
# ЕДИНАЯ игровая (env+wall+kid).
|
# ЕДИНАЯ игровая (env+wall+kid).
|
||||||
KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl)
|
KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl)
|
||||||
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl
|
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl
|
||||||
|
# Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его
|
||||||
|
# делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже.
|
||||||
|
KID_BIN := $(KID_DIR)/kid_data.bin
|
||||||
|
GUARD_DIR := $(CURDIR)/../poc/res/guard
|
||||||
|
GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \
|
||||||
|
$(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl
|
||||||
|
|
||||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(LVL_DATA)
|
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(LVL_DATA)
|
||||||
include $(PROJ_ROOT)/app.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
|
|
||||||
|
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
||||||
|
# чтобы корень диска не зарастал десятками .atl.
|
||||||
|
# BG\ — фон (env/wall/fore/pot)
|
||||||
|
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
|
||||||
|
# GUARD\ — стражи (появятся здесь же)
|
||||||
|
# LEVELS\ — уровни
|
||||||
|
hdd: $(EXAMPLE).exe
|
||||||
|
$(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \
|
||||||
|
$(EXAMPLE).exe \
|
||||||
|
$(foreach f,$(BG_DATA),BG:$(f)) \
|
||||||
|
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
||||||
|
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
||||||
|
$(foreach f,$(LVL_DATA),LEVELS:$(f))
|
||||||
|
|
||||||
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
||||||
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
|
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
|
||||||
cd $(TC) && python3 pop_pack_bg.py
|
cd $(TC) && python3 pop_pack_bg.py
|
||||||
|
$(GUARD_DATA): $(TC)/pop_pack_guard.py
|
||||||
|
cd $(TC) && python3 pop_pack_guard.py
|
||||||
|
|
||||||
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
||||||
cd $(TC) && python3 pop_pack_kid.py
|
cd $(TC) && python3 pop_pack_kid.py
|
||||||
kid_data.h: $(TC)/pop_extract_kid_data.py
|
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
|
||||||
cd $(TC) && python3 pop_extract_kid_data.py
|
cd $(TC) && python3 pop_extract_kid_data.py
|
||||||
|
|
||||||
$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(LVL_DATA)
|
$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_draw.c _pop_draw.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
||||||
|
|||||||
@@ -39,9 +39,14 @@ ESC выход.
|
|||||||
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
|
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
|
||||||
(оба генерируются скриптами `../toolchain/`).
|
(оба генерируются скриптами `../toolchain/`).
|
||||||
|
|
||||||
## Статус
|
## Статус (2026-08-01)
|
||||||
|
|
||||||
Готово и проверено в MAME: статический фон комнаты 1; Kid — анимация,
|
Играется весь уровень 1: комнаты и переходы между ними, Kid (анимация,
|
||||||
управление, коллизия/падение, зацеп/подтягивание/спуск, fore-окклюзия
|
управление, коллизия/падение, зацеп/подтягивание/спуск, окклюзия), кнопки и
|
||||||
пола/стены над Kid. В работе: проваливающиеся полы (loose floors) — см.
|
ворота, пики, проваливающиеся полы, дверь уровня, меч и бой, стражи с ИИ,
|
||||||
`../docs/loose_floors_plan.md`.
|
HP и зелья.
|
||||||
|
|
||||||
|
Не сделано: выход в следующий уровень (дверь открывается, но войти в неё
|
||||||
|
нельзя), звук, таймер/HUD времени, сохранения. **Что берём в работу
|
||||||
|
сейчас — [`TASKS.md`](TASKS.md)**; план следующих уровней —
|
||||||
|
[`../docs/levels_plan.md`](../docs/levels_plan.md).
|
||||||
|
|||||||
@@ -0,0 +1,822 @@
|
|||||||
|
# roomtest — доска текущих задач (обновлено 2026-08-04)
|
||||||
|
|
||||||
|
Не список багов (открытые — [`bug_list.md`](bug_list.md), закрытые с разбором
|
||||||
|
корней — [`bug_closed.md`](bug_closed.md)) и не план фаз
|
||||||
|
(`../docs/PORT_PLAN.md`, `../docs/layout_plan_v2.md`, `../docs/levels_plan.md`),
|
||||||
|
а то, **что берём в работу сейчас и в каком порядке**. Каждая запись: что
|
||||||
|
сделать, почему именно сейчас, чем подтверждать результат.
|
||||||
|
|
||||||
|
**Открытых багов нет** ([`bug_list.md`](bug_list.md)): остались незакрытые
|
||||||
|
оптимизации [T-1](bug_list.md#t-1), [T-2](bug_list.md#t-2) и незаконченная
|
||||||
|
[таблица обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1).
|
||||||
|
|
||||||
|
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
|
||||||
|
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
|
||||||
|
гипотезой (memory `defer_unexplained_quirks`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ТЕКУЩАЯ ЦЕЛЬ: уровни 1–3 (подземелье) отлажены целиком
|
||||||
|
|
||||||
|
Решение 2026-08-04: **palace (уровни 4+) откладываем**, доводим до
|
||||||
|
играбельности три dungeon-уровня. Основание — они не требуют ни одного
|
||||||
|
нового ассета фона: инвентарь тайлов, снятый с `res200N.bin`, показывает,
|
||||||
|
что новое появляется только так —
|
||||||
|
|
||||||
|
```
|
||||||
|
ур. 1 empty, floor, spike, pillar, gate, closer, potion, loose, debris,
|
||||||
|
opener, level_door L/R, torch, wall, skeleton, sword ← всё есть
|
||||||
|
ур. 2 bigpillar_bottom(8), bigpillar_top(9), doortop(12) ← есть (2026-08-04)
|
||||||
|
ур. 3 chomper(18) ← НЕТ механики
|
||||||
|
ур. 4 lattice_pillar(25)…lattice_right(29) + тайлсет palace ← отложено
|
||||||
|
```
|
||||||
|
|
||||||
|
**Порядок (решение пользователя 2026-08-04): СНАЧАЛА приёмка уровня 2,
|
||||||
|
потом уровень 3.** Причина техническая, а не вкусовая: чомперы лягут в
|
||||||
|
тот же банк 2, где живёт отрисовка, и чинить баги фона поверх свежей
|
||||||
|
механики дороже, чем до неё.
|
||||||
|
|
||||||
|
| # | Задача | Что | Блокирует |
|
||||||
|
|---|--------|-----|-----------|
|
||||||
|
| 1 | [L2-PASS](#l2-pass) | приёмка уровня 2 + баги отрисовки | ← **сейчас** |
|
||||||
|
| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 |
|
||||||
|
| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 |
|
||||||
|
| 4 | [L3-CHKP](#l3-chkp) | чекпойнт ур. 3 | корректный респавн ур. 3 |
|
||||||
|
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
|
||||||
|
| — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 |
|
||||||
|
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
|
||||||
|
| ✔ | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | **СДЕЛАНО 2026-08-05** |
|
||||||
|
| ✔ | [MEM-BANK5](#mem-bank5) | `pop_ctrl.c` → банк 5 | **СДЕЛАНО 2026-08-05**: куча 180 Б → 2298 Б |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3
|
||||||
|
|
||||||
|
Сюда попадает то, что уже работает в проверочном прогоне, но должно быть
|
||||||
|
подтверждено на сквозных прогонах уровней — потому что задевает механику
|
||||||
|
шире, чем собственный сценарий.
|
||||||
|
|
||||||
|
- **Зацеп ПРЯМО В ПРЫЖКЕ** (`POP_ENABLE_JUMP_GRAB`, `pop_tune.h`, сделан
|
||||||
|
2026-08-06, предварительно проверен пользователем). Почему нужен именно
|
||||||
|
финальный прогон: точки вызова стоят не только в `check_action`, но и в
|
||||||
|
ОБЕИХ ветках `check_bumped` — то есть код вклинивается перед обычным
|
||||||
|
ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о
|
||||||
|
стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На
|
||||||
|
уровнях 1–3 это надо специально потрогать в паре мест каждого уровня.
|
||||||
|
Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — `enable_jump_grab`),
|
||||||
|
так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при
|
||||||
|
включённых enhancements.
|
||||||
|
|
||||||
|
## P0 — делаем сейчас
|
||||||
|
|
||||||
|
### <a id="l2-pass"></a>L2-PASS. Приёмка уровня 2 — **SMOKE ПРОЙДЕН 2026-08-05**
|
||||||
|
|
||||||
|
> **Прогон 2026-08-05 (пользователь): уровень 2 пройден.** Полная приёмка по
|
||||||
|
> всем комнатам — позже; из smoke пришли [BUG-LOOSE-3](bug_list.md#bug-loose-3)
|
||||||
|
> и [BUG-GUARD-DEAF-1](bug_list.md#bug-guard-deaf-1). Следом прогнан smoke
|
||||||
|
> уровня 3 — его наблюдения в [разделе «Уровень 3»](bug_list.md#уровень-3).
|
||||||
|
|
||||||
|
Баги отрисовки уровня 2 пользователь подаёт списком отдельно — они идут в
|
||||||
|
[`bug_list.md`](bug_list.md), раздел «Уровень 2». Ниже — **карта
|
||||||
|
содержимого уровня, снятая прямо с `res2002.bin`**, чтобы приёмка шла по
|
||||||
|
списку, а не «на глаз»: если механика в таблице есть, а в игре не
|
||||||
|
сработала — это баг, а не «так задумано».
|
||||||
|
|
||||||
|
**Стражи — 5, в комнатах 4, 7, 11, 15, 24** (skill 1/2/1/1/3, цвета
|
||||||
|
1/3/1/1/6 — цвет мы пока игнорируем, атлас один). Заметить: страж
|
||||||
|
комнаты 24 со skill 3 — первый по-настоящему опасный.
|
||||||
|
|
||||||
|
**Кнопки и что они открывают** (декодировано из LINKLOC/LINKMAP):
|
||||||
|
|
||||||
|
| Кнопка | Тип | Цель |
|
||||||
|
|--------|-----|------|
|
||||||
|
| к.9 @ряд1,кол1 | RAISE | **дверь уровня** к.23 @1,3 — то есть выход |
|
||||||
|
| к.11 @1,1 | RAISE | ворота к.18 @0,9 |
|
||||||
|
| к.18 @0,7 | RAISE | ворота к.7 @0,9 **и** к.18 @0,9 (две сразу) |
|
||||||
|
| к.18 @0,2 | DROP | закрывает ворота к.7 @0,9 |
|
||||||
|
| к.13 @1,4 | DROP | закрывает ворота к.13 @1,5 |
|
||||||
|
|
||||||
|
**Ловушки и предметы по комнатам:**
|
||||||
|
|
||||||
|
```
|
||||||
|
к. 3 loose@2,2 зелье@2,5 (здоровье)
|
||||||
|
к. 4 loose@1,7 loose@1,8 + СТРАЖ
|
||||||
|
к. 5 дверь уровня @1,2-3 — ВХОД (захлопывается за спиной)
|
||||||
|
к. 6 пики@1,3 loose@1,5 зелье@2,7 (здоровье)
|
||||||
|
к. 7 ворота@0,9 пики@2,7 + СТРАЖ
|
||||||
|
к. 8 зелье@2,2 (здоровье)
|
||||||
|
к. 9 кнопка RAISE@1,1 loose@2,6
|
||||||
|
к.10 пики@2,2
|
||||||
|
к.11 кнопка RAISE@1,1 + СТРАЖ
|
||||||
|
к.12 зелье@2,6 (здоровье) loose@2,8
|
||||||
|
к.13 зелье@1,3 (ВРЕДНОЕ, −1 HP) кнопка DROP@1,4 ворота@1,5 зелье@1,8
|
||||||
|
к.15 + СТРАЖ
|
||||||
|
к.18 кнопка DROP@0,2 loose@0,4 кнопка RAISE@0,7 ворота@0,9 зелье@2,6
|
||||||
|
к.19 пики@1,4
|
||||||
|
к.20 ЗЕЛЬЕ@1,2 — БОЛЬШАЯ СКЛЯНКА (+1 к потолку HP) пики@2,6 пики@2,7
|
||||||
|
к.23 дверь уровня @1,3-4 — ВЫХОД
|
||||||
|
к.24 + СТРАЖ (skill 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Отдельно проверить то, что сделано именно сегодня и на уровне 1 не
|
||||||
|
проверялось:**
|
||||||
|
- **большая склянка** (к.20) — потолок HP становится 4, индикатор рисует
|
||||||
|
четыре деления, и это HP **переносится на уровень 3**;
|
||||||
|
- **меч уже в руках** с самого старта (`have_sword = level >= 2`) — на
|
||||||
|
уровне 1 его надо было подбирать;
|
||||||
|
- **выход через дверь к.23** вживую (кнопка в к.9) — путь тот же, что
|
||||||
|
закрыт на уровне 1 (L1-EXIT), но на этом уровне не прогонялся;
|
||||||
|
- **смерть/респавн** возвращают на уровень 2, а не на 1.
|
||||||
|
|
||||||
|
### <a id="rooms-graph"></a>Связность комнат уровней 1–3 (снято 2026-08-05)
|
||||||
|
|
||||||
|
Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет
|
||||||
|
соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись
|
||||||
|
13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый,
|
||||||
|
в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии.
|
||||||
|
|
||||||
|
| уровень | старт | недостижимы | признак |
|
||||||
|
|---------|-------|-------------|---------|
|
||||||
|
| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет |
|
||||||
|
| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы |
|
||||||
|
| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** |
|
||||||
|
|
||||||
|
```
|
||||||
|
ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7
|
||||||
|
23 R→22, у 22 L=4 | 24 R→7, у 7 L=2
|
||||||
|
| 24 U→16, у 16 D=0
|
||||||
|
на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty
|
||||||
|
```
|
||||||
|
|
||||||
|
То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в
|
||||||
|
брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот
|
||||||
|
же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю
|
||||||
|
колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов
|
||||||
|
там низкий, в игре их не видно.
|
||||||
|
|
||||||
|
**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная
|
||||||
|
связь — вверх, откуда Кид падает):
|
||||||
|
|
||||||
|
```
|
||||||
|
к.10 U→4 пики(2,2) + пол — падение из комнаты 4
|
||||||
|
к.14 U→21 шахта 2 тайла шириной, дно = обломки
|
||||||
|
к.17 U→15 то же
|
||||||
|
```
|
||||||
|
|
||||||
|
Это не баги данных: в 14/17 попадают только падением насмерть, а из 10
|
||||||
|
(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать
|
||||||
|
«застрял» багом.
|
||||||
|
|
||||||
|
### <a id="l3-chomp"></a>L3-CHOMP. Чомперы — механика уровня 3
|
||||||
|
|
||||||
|
**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти
|
||||||
|
на каждом уровне, так что это вложение не только в ур. 3.
|
||||||
|
|
||||||
|
**Что портировать:** `animate_chomper` (seg007) — состояние в `room_modif`,
|
||||||
|
как у пик/ворот, значит шаблон уже отработан (`gates_spikes_plan.md`);
|
||||||
|
коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка
|
||||||
|
кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`).
|
||||||
|
|
||||||
|
**Риск:** банк 2 (`pop_bg`) занят на **86.5 %, свободно 2213 Б** — считать
|
||||||
|
место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся
|
||||||
|
в потолок» (коммит 2f3e854). Свободные номера банков есть (5+).
|
||||||
|
|
||||||
|
### <a id="l3-skel"></a>L3-SKEL. Скелет — единственный противник уровня 3
|
||||||
|
|
||||||
|
**Важно:** в данных уровня 3 **стражей нет вообще** (`guards_tile` пуст во
|
||||||
|
всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных»,
|
||||||
|
а **спецсобытие** `check_skel` (seg002:1044):
|
||||||
|
|
||||||
|
> в комнате 1, когда `Kid.curr_col` == 2 или 3 и дверь уровня открыта,
|
||||||
|
> тайл `tiles_21_skeleton` (комната 1, тайлпос 15) стирается в пол, а на
|
||||||
|
> его месте поднимается персонаж: `charid_4_skeleton`, меч сразу вынут,
|
||||||
|
> `seq_88_skel_wake_up`, skill 2, HP 3.
|
||||||
|
|
||||||
|
Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают.
|
||||||
|
|
||||||
|
**Что нужно:**
|
||||||
|
- `charid_4_skeleton` в `enter_guard`/`pop_guard_enter` (seg002:196): при
|
||||||
|
`tbl_guard_type[level] == 2` персонаж поднимается **с вынутым мечом** и
|
||||||
|
последовательностью `seq_63_guard_active_after_fall`, а не
|
||||||
|
`seq_77_guard_stand_inactive`;
|
||||||
|
- скелет **бессмертен** — чит `K` его уже не берёт (`pop_guard_kill`
|
||||||
|
проверяет `CHARID_4_SKELETON`), но и боёвка должна возвращать его к
|
||||||
|
жизни (seg002:252);
|
||||||
|
- **новый атлас**: `../SDLPoP/data/SKEL/` (29 файлов) — `pop_pack_guard.py`
|
||||||
|
сейчас прибит к `GUARD/`, нужен параметр набора (`tbl_guard_dat` =
|
||||||
|
GUARD/FAT/SKEL/VIZIER/SHADOW).
|
||||||
|
|
||||||
|
### <a id="l3-chkp"></a>L3-CHKP. Чекпойнт уровня 3
|
||||||
|
|
||||||
|
`level3_set_chkp` (seg002:519): вход в комнату 7 ставит `checkpoint = 1` и
|
||||||
|
`hitp_beg_lev = hitp_max`. `do_startpos` (seg003:141) при `checkpoint`
|
||||||
|
подменяет старт: комната 2, тайлпос 6, направление влево, и убирает
|
||||||
|
loose-плиту (комната 7, колонка 4, ряд 0). Механика `hitp_beg_lev` уже
|
||||||
|
есть (сделана в L2), остаётся сам флаг и подмена старта.
|
||||||
|
|
||||||
|
|
||||||
|
### <a id="tune-1"></a>TUNE-1. Параметры движка — в конфиг, а не в код
|
||||||
|
|
||||||
|
**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном
|
||||||
|
заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки
|
||||||
|
(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`).
|
||||||
|
Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а
|
||||||
|
не константой по месту.
|
||||||
|
|
||||||
|
**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без
|
||||||
|
пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под
|
||||||
|
моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа,
|
||||||
|
комментарии `;`, неизвестные ключи игнорировать), файл необязателен —
|
||||||
|
нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`,
|
||||||
|
`[debug]`, `[enhancements]`.
|
||||||
|
|
||||||
|
**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) +
|
||||||
|
`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его
|
||||||
|
(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и
|
||||||
|
опций — часть задачи: у него уже разложены по группам стартовые
|
||||||
|
HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало,
|
||||||
|
чекпойнт), тайминги ворот и пик, скорости, а отдельной группой —
|
||||||
|
`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже
|
||||||
|
портировали). Брать всё подряд не надо: переносим по мере того, как
|
||||||
|
константа реально понадобилась в игре.
|
||||||
|
|
||||||
|
**Оговорка по памяти.** Парсер и таблица параметров — холодный код,
|
||||||
|
исполняется один раз при старте: кандидат в банк, а не в резидент W1.
|
||||||
|
|
||||||
|
### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН**
|
||||||
|
|
||||||
|
> **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как
|
||||||
|
> установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой
|
||||||
|
> замер потока байт это не подтвердил: клавиатура MAME-Sprinter
|
||||||
|
> (`pc_kbd ms_naturl`) обёртку `E0 F0 12` / `E0 12` не шлёт вовсе — при
|
||||||
|
> зажатом Shift поток на стрелку ровно `E0 75 E0 75 …`. Значит удвоения
|
||||||
|
> трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO
|
||||||
|
> переполняется» отпадает; сам ФИКС (плотный опрос `kbd_raw_poll`) остаётся
|
||||||
|
> верным и нужным — переполнение вызывает не Shift, а короткая жизнь
|
||||||
|
> импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия
|
||||||
|
> — [BUG-KBD-5](bug_closed.md#bug-kbd-5).
|
||||||
|
>
|
||||||
|
> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: при
|
||||||
|
> зажатом Shift PS/2 удваивает трафик («fake shift»), а импульс запроса
|
||||||
|
> прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO
|
||||||
|
> SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен
|
||||||
|
> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) —
|
||||||
|
> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча.
|
||||||
|
>
|
||||||
|
> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home →
|
||||||
|
> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже:
|
||||||
|
> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147.
|
||||||
|
> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в
|
||||||
|
> простое). Ниже — полный протокол, как к этому пришли.
|
||||||
|
>
|
||||||
|
> **ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно
|
||||||
|
> лучше, но иногда при зажатом Shift стрелка всё-таки пропускается».**
|
||||||
|
> Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали,
|
||||||
|
> значит остаточная частота заметно ниже прежних ~15 %. **Задача осознанно
|
||||||
|
> ОТЛОЖЕНА до финальной полировки всей программы** (решение пользователя);
|
||||||
|
> сейчас клавиатура пригодна для работы.
|
||||||
|
>
|
||||||
|
> **Где именно осталась дыра — чтобы на полировке не начинать с нуля.**
|
||||||
|
> Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это
|
||||||
|
> занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при
|
||||||
|
> допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё
|
||||||
|
> ещё может потерять байт — ровно «иногда». Порядок действий, если
|
||||||
|
> вернёмся:
|
||||||
|
> 1. Вернуть вызовы `kbd_raw_poll()` между блитами занятой фазы (они
|
||||||
|
> бесплатны; сами по себе не помогали, но вместе с хуком закрывают
|
||||||
|
> именно этот зазор) и при необходимости внутрь тайловых циклов
|
||||||
|
> `pop_bg` — тогда слепым остаётся только тело одного блита.
|
||||||
|
> 2. Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые
|
||||||
|
> нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий.
|
||||||
|
> 3. Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code
|
||||||
|
> Set 3 через BIOS `$EA` (убирает «fake shift» в корне, но в MAME
|
||||||
|
> непроверяемо — обратный путь к клавиатуре не разведён) и общий
|
||||||
|
> `m_irq_off_timer` в драйвере MAME.
|
||||||
|
|
||||||
|
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
|
||||||
|
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
|
||||||
|
нажатия ← не отрабатываются, пока Shift не отпустишь.
|
||||||
|
|
||||||
|
**Рабочая гипотеза (механизм, а не догадка «что-то с клавиатурой»).**
|
||||||
|
Три известных факта складываются в одну картину:
|
||||||
|
|
||||||
|
1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ
|
||||||
|
клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием
|
||||||
|
шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было
|
||||||
|
бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3). То есть
|
||||||
|
ровно в связке Shift+стрелка трафик удваивается.
|
||||||
|
2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то,
|
||||||
|
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
|
||||||
|
«нажатие не отработало»; потерянный break = залипание (его лечит
|
||||||
|
`kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш).
|
||||||
|
3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.**
|
||||||
|
`mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()`
|
||||||
|
выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая
|
||||||
|
запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же
|
||||||
|
заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию.
|
||||||
|
**Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт
|
||||||
|
остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц).
|
||||||
|
4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит
|
||||||
|
(`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»);
|
||||||
|
порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это
|
||||||
|
сотни микросекунд, на порядки больше 32-тактового импульса.
|
||||||
|
|
||||||
|
Отсюда: **обе версии пользователя — про одно и то же.** Логика ввода
|
||||||
|
(`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с SDLPoP и
|
||||||
|
выглядит корректной: `safe_step()` ставит `control_forward = CONTROL_IGNORE`,
|
||||||
|
и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть
|
||||||
|
повторные тапы ← при зажатом Shift обязаны работать. Не работают они
|
||||||
|
потому, что до нас не доезжает либо make, либо break стрелки.
|
||||||
|
|
||||||
|
**План проверки — по шагам, каждый даёт артефакт:**
|
||||||
|
|
||||||
|
1. Счётчики в MAME: брейк на `_kbdraw_overrun` (запись) и на ветке
|
||||||
|
`tr_kbd_drain` — сколько overrun'ов за 10 с при «Shift зажат, тапаю ←»
|
||||||
|
против «тапаю ← без Shift». Ожидание по гипотезе: с Shift кратно больше.
|
||||||
|
2. Замер максимального DI-окна кадра: брейкпоинты на `di`/`ei` в
|
||||||
|
`_bgi_blit_cols_raw` + `{printf totalcycles; g}` — получить реальную длину
|
||||||
|
в тактах и в микросекундах.
|
||||||
|
3. **Спайк «блит без DI».** `docs/new/06-accel.md §6.6`: новая прошивка
|
||||||
|
допускает работу акселератора при EI (по приходу прерывания он
|
||||||
|
отключается, по `RETI` включается обратно); старая — нет. Собрать libbgi
|
||||||
|
с убранным `di` в блит/heal-ядрах, прогнать roomtest в MAME: (а) не
|
||||||
|
рушится ли картинка, (б) падает ли счётчик overrun из п.1. Если да —
|
||||||
|
причина подтверждена, и дальше это вопрос «какая прошивка на живом
|
||||||
|
железе» (по умолчанию оставить DI, режим без DI — опцией libbgi).
|
||||||
|
4. **Независимо от п.3 — `kbd_raw_poll()`.** Вычерпывание FIFO ОПРОСОМ
|
||||||
|
(порт `0x19` бит 0 → читать `0x18`, тот же декодер make/break, что в
|
||||||
|
трамплине) из главного цикла 2–4 раза за кадр между фазами `PROF()`.
|
||||||
|
Снимает зависимость от «поймали ли мы импульс IRQ» вообще, стоит сотни
|
||||||
|
тактов, графику не трогает. Реализация: вынести drain-цикл из
|
||||||
|
`libc/irq/_irq_tramp.c` в общий кусок либо продублировать в
|
||||||
|
`libc/kbd/kbd_raw_poll.c`; тело обязано идти под `DI` (гонка с ISR за
|
||||||
|
деструктивное чтение порта 0x18).
|
||||||
|
5. Побочно сюда же играет **[T-2](bug_list.md#t-2) (idle-skip)**: не
|
||||||
|
перерисовывать Кида, пока поза/координаты не менялись, — это минус
|
||||||
|
heal+blit (то есть минус DI-окна) в самых спокойных кадрах, где как раз
|
||||||
|
и тапают Shift+стрелку.
|
||||||
|
6. Только если после 3–4 симптом жив — копать логику
|
||||||
|
`control_shift2`/`CONTROL_IGNORE` против `seg005.c:374..390`.
|
||||||
|
|
||||||
|
**Критерий готовности:** при зажатом Shift десять тапов ← дают десять
|
||||||
|
осторожных шагов (проверка в MAME через `:kbd:ms_naturl:*` напрямую, НЕ
|
||||||
|
через `press_key` — тот дёргает обе клавиатуры, см. `docs/libc-reference.md`
|
||||||
|
`<kbd_raw.h>`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### KBD-1: ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась
|
||||||
|
|
||||||
|
**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не
|
||||||
|
спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же
|
||||||
|
`E0`-префикс и тот же «fake shift», что у стрелок), но игрой игнорируется,
|
||||||
|
поэтому Кид стоит на месте и рельеф комнаты на результат не влияет.
|
||||||
|
Брейкпоинты с действием `{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки
|
||||||
|
машины) в трёх точках: вход клавиатурной ветки трамплина, чтение порта 0x18
|
||||||
|
внутри drain-цикла, запись make-бита для кода `0x6C`. Скратч-байты — хвост
|
||||||
|
`ovr_tile[]` (в этом сценарии не используется).
|
||||||
|
|
||||||
|
**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до
|
||||||
|
декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил
|
||||||
|
пользователь.
|
||||||
|
|
||||||
|
| Прогон | make дошло / нажато | overrun |
|
||||||
|
|--------|---------------------|---------|
|
||||||
|
| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 |
|
||||||
|
| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 |
|
||||||
|
| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 |
|
||||||
|
|
||||||
|
**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет
|
||||||
|
и прерывания разрешены практически всё время, потерь НЕ меньше, а больше.
|
||||||
|
|
||||||
|
**Вывод 2: `kbd_raw_poll()` в текущей расстановке бесполезен** — 9/10 и с
|
||||||
|
ним, и без. Причина понятна задним числом: шесть вызовов стоят В ТЕХ ЖЕ
|
||||||
|
точках, где прерывания и так разрешены, то есть добавляют ровно то, что
|
||||||
|
трамплин сделал бы сам. Вызовы из `roomtest.c` убраны; сама функция в libc
|
||||||
|
оставлена — она корректна и нужна как заготовка под «плотный опрос» (см.
|
||||||
|
ниже), но в горячем цикле её держать не за что.
|
||||||
|
|
||||||
|
**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта
|
||||||
|
0x18: 5 нажатий Shift+Home должны дать ровно 50 байт (нажатие `E0 F0 12` +
|
||||||
|
`E0 6C`, отпускание `E0 F0 6C` + `E0 12` = по 10 на цикл). Насчитано **49**
|
||||||
|
— и ровно один make потерян. То есть до процессора байт не доехал вообще,
|
||||||
|
декодер тут ни при чём.
|
||||||
|
|
||||||
|
**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же
|
||||||
|
49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина
|
||||||
|
(1.75 байта за вход). То есть больше сорока процентов импульсов запроса
|
||||||
|
не были обслужены, и байты копятся в трёхбайтовом FIFO вплотную к его
|
||||||
|
потолку; одна неудачная пауза — и байт потерян.
|
||||||
|
|
||||||
|
### KBD-1: ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью
|
||||||
|
|
||||||
|
`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме
|
||||||
|
`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода:
|
||||||
|
любая работа разредила бы опрос и испортила замер). Это физический
|
||||||
|
максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики.
|
||||||
|
|
||||||
|
| Прогон | нажатий | make дошло | байт прочитано / ожидалось |
|
||||||
|
|--------|---------|-----------|-----------------------------|
|
||||||
|
| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) |
|
||||||
|
| Shift + Home | **25** | **25** | **250 / 250** |
|
||||||
|
|
||||||
|
**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах
|
||||||
|
за кадр терялся 1 байт из 50. При такой частоте потерь вероятность
|
||||||
|
случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не
|
||||||
|
совпадение.
|
||||||
|
|
||||||
|
**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно
|
||||||
|
опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за
|
||||||
|
60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого.
|
||||||
|
|
||||||
|
**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято
|
||||||
|
работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая
|
||||||
|
луч. Опрос там стоит ноль и покрывает две трети периода с запасом по
|
||||||
|
плотности. Остаётся слепым только тело одного accel-блита под DI (до
|
||||||
|
~650 мкс) — разорвать его нельзя (см. «что НЕ делать»).
|
||||||
|
|
||||||
|
**Почему нужна именно такая частота (вопрос «PS/2 же не даёт больше 30
|
||||||
|
нажатий в секунду»).** Частота опроса определяется НЕ темпом нажатий, а
|
||||||
|
темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Одно нажатие при
|
||||||
|
зажатом Shift — это 5 байт подряд (`E0 F0 12`, `E0 6C`), отпускание — ещё 5,
|
||||||
|
и клавиатура выдаёт их со скоростью провода: 11 бит на байт при ~10–16 кГц
|
||||||
|
= ~0.7–1.1 мс на байт. Воронка — 3 байта. Значит между двумя вычерпываниями
|
||||||
|
имеют право прийти максимум два байта, то есть вычерпывать надо не реже чем
|
||||||
|
раз в ~1.5–2 мс (0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду
|
||||||
|
переполнит FIFO**, если в эти несколько миллисекунд его никто не разгребает.
|
||||||
|
Замер это подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости.
|
||||||
|
В норме разгребает прерывание; опрос понадобился только потому, что ~44 %
|
||||||
|
импульсов здесь теряется.
|
||||||
|
|
||||||
|
**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не
|
||||||
|
ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md`
|
||||||
|
§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать:
|
||||||
|
- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт,
|
||||||
|
break = 2. Нажатие с шифтом перестаёт превышать FIFO в принципе.
|
||||||
|
- либо хотя бы отключить typematic (`0xF5`/`0xF7`).
|
||||||
|
|
||||||
|
**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только
|
||||||
|
направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь
|
||||||
|
(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда.
|
||||||
|
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
|
||||||
|
кандидат на «когда дойдём до реального железа», а не на сейчас.
|
||||||
|
|
||||||
|
**Что сделано по этому плану (2026-08-01):**
|
||||||
|
1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
|
||||||
|
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
|
||||||
|
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
|
||||||
|
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
|
||||||
|
нужен push/pop, а сам таймаут в итерациях станет длиннее по времени.
|
||||||
|
**Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда,
|
||||||
|
где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60. Полезной
|
||||||
|
работы не отнимается нисколько.
|
||||||
|
**Ограничитель области, если «постоянный опрос» всё равно не нравится:**
|
||||||
|
потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift
|
||||||
|
потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt —
|
||||||
|
тогда опрос работает исключительно в той ситуации, ради которой заведён.
|
||||||
|
2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет.
|
||||||
|
3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило.
|
||||||
|
Держать в уме, если на реальном железе или на более тяжёлых сценах
|
||||||
|
(несколько стражей) потери появятся снова — накрыть блиты дешевле, чем
|
||||||
|
изобретать что-то новое.
|
||||||
|
4. ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые
|
||||||
|
нажатия ровнее человеческих, и «залипания до отпускания Shift» они не
|
||||||
|
воспроизводили с самого начала.
|
||||||
|
|
||||||
|
**Куда смотреть дальше, если плотного опроса не хватит.**
|
||||||
|
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
|
||||||
|
его машине занимает часы). Для протокола, подозрение осталось:
|
||||||
|
`sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта
|
||||||
|
CPU**, и `m_irq_off_timer` — **один на два источника** (`irq_on()` экрана
|
||||||
|
заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое
|
||||||
|
прерывание способно обрезать клавиатурный импульс — правдоподобное
|
||||||
|
объяснение «44 % пропущенных импульсов».
|
||||||
|
2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе
|
||||||
|
проблемы может не быть вовсе. Проверять при первом прогоне на живом
|
||||||
|
Sprinter.
|
||||||
|
|
||||||
|
**Про совпадение кадрового и клавиатурного прерываний** (вопрос
|
||||||
|
пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором
|
||||||
|
`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл,
|
||||||
|
значит экран»; совпадение возможно, но «исключительно редкий случай»
|
||||||
|
(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин
|
||||||
|
делает ровно предписанное. Известный побочный эффект: при совпадении мы
|
||||||
|
обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на
|
||||||
|
потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает
|
||||||
|
повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка,
|
||||||
|
в KBD-1 не входит.
|
||||||
|
|
||||||
|
**Что НЕ делать (проверено, стоило времени):**
|
||||||
|
- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di`→`nop` в
|
||||||
|
`_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в
|
||||||
|
памяти **уронил машину в перезагрузку**. То есть режим «акселератор
|
||||||
|
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
|
||||||
|
недоступен — вопрос закрыт артефактом, а не рассуждением.
|
||||||
|
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01**
|
||||||
|
|
||||||
|
> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту,
|
||||||
|
> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах
|
||||||
|
> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид
|
||||||
|
> стоит, стража нет):
|
||||||
|
>
|
||||||
|
> | путь | тактов на кадр | кадров в выборке |
|
||||||
|
> |------|----------------|------------------|
|
||||||
|
> | клипающее ядро (как было) | **26 200** | 149 |
|
||||||
|
> | noclip (стало) | **15 848** | 239 |
|
||||||
|
>
|
||||||
|
> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при
|
||||||
|
> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина —
|
||||||
|
> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то
|
||||||
|
> есть на той же геометрии и в той же сцене.
|
||||||
|
>
|
||||||
|
> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17),
|
||||||
|
> BANK2 13 792 → **13 676** (−116, свободно стало 2708 Б — это тот самый
|
||||||
|
> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (−263),
|
||||||
|
> BANK4 без изменений.
|
||||||
|
>
|
||||||
|
> **Грабли, стоившие двух пересборок** (вынесено в память
|
||||||
|
> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static
|
||||||
|
> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое
|
||||||
|
> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит
|
||||||
|
> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в
|
||||||
|
> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном
|
||||||
|
> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без
|
||||||
|
> трамплина, как у `pop_sword_draw`).
|
||||||
|
>
|
||||||
|
> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за
|
||||||
|
> решёткой шва» (straddle — там как раз работает клипающий фолбэк) —
|
||||||
|
> артефактов и следов нет.
|
||||||
|
|
||||||
|
**Ниже — исходная постановка задачи (что и почему смотрели).**
|
||||||
|
|
||||||
|
|
||||||
|
**Зачем сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта
|
||||||
|
стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против
|
||||||
|
4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая
|
||||||
|
оставшаяся оптимизация: не переписывание логики, а выбор ядра.
|
||||||
|
|
||||||
|
**Что уже правильно** (шаблон, который надо распространить): блиты Кида,
|
||||||
|
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
|
||||||
|
`gfx_blit_cols_part_noclip`, иначе в клипающий вариант
|
||||||
|
(`pop_kid.c:86,591,676`, `pop_gdraw.c:105`).
|
||||||
|
|
||||||
|
**Что чинили:**
|
||||||
|
|
||||||
|
- ✅ `kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с
|
||||||
|
клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им
|
||||||
|
уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего
|
||||||
|
ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий
|
||||||
|
`pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`).
|
||||||
|
- ⛔ `pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`:
|
||||||
|
**оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный
|
||||||
|
параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт
|
||||||
|
`border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки
|
||||||
|
тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы
|
||||||
|
не «оптимизировать» повторно.
|
||||||
|
|
||||||
|
**Что обязано остаться с клипом** (зафиксировано комментариями в коде):
|
||||||
|
- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется
|
||||||
|
по построению;
|
||||||
|
- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной)
|
||||||
|
и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`;
|
||||||
|
- борта поля (`pop_room_clip_borders`) — см. выше.
|
||||||
|
|
||||||
|
**Что уже было правильно** (шаблон, который и распространили): блиты Кида,
|
||||||
|
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
|
||||||
|
`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip
|
||||||
|
БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по
|
||||||
|
фиксированным координатам; это тоже помечено в коде.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — сразу после P0 (закрываем уровень 1 как ИГРУ, а не стенд)
|
||||||
|
|
||||||
|
### L1-START. Старт по данным уровня — **СДЕЛАНО 2026-08-01**
|
||||||
|
Старт и оба рестарта (смерть, выпадение из уровня) сведены в один
|
||||||
|
`pop_start_level()` — порт `start_level` + `do_startpos` + `set_start_pos`
|
||||||
|
(seg003): комната/тайл/направление берутся из `pop_level_start_*`, направление
|
||||||
|
инвертируется (`~start_dir`), поза входа — из `tbl_entry_pose`. У уровня 1
|
||||||
|
это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что
|
||||||
|
захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение
|
||||||
|
на row 1 — как по данным уровня.
|
||||||
|
|
||||||
|
`#define ROOMNAV` оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в
|
||||||
|
оригинале не было, — как и `S` (выдать меч), `K`, `I`. Все они со временем
|
||||||
|
съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01).
|
||||||
|
|
||||||
|
### L1-EXIT. Выход с уровня (дверь уровня) — **СДЕЛАНО 2026-08-01**
|
||||||
|
Портированы: ветка двери уровня из `up_pressed` + `go_up_leveldoor`
|
||||||
|
(seg005:0482/0574) — в `pop_leveldoor_enter()` (`pop_map.c`, тайлы и
|
||||||
|
геометрия) и `up_pressed()` (`pop_ctrl.c`, только последовательность);
|
||||||
|
опкод `0xF1 END_LEVEL` в `play_seq` теперь инкрементит `pop_next_level`
|
||||||
|
(порт `next_level`), а главный цикл по нему перезапускает уровень — ровно
|
||||||
|
та точка, куда `levels_plan.md` §2.2 подключит загрузку уровня 2.
|
||||||
|
|
||||||
|
Открытость двери проверяем по `modifier >= 42` (ветка `fix_exit_door`), а не
|
||||||
|
по ванильному `leveldoor_open`: с ванильным условием можно войти в ещё
|
||||||
|
ползущую створку.
|
||||||
|
|
||||||
|
**Грабли, которые стоили отдельного разбора:** `go_up_leveldoor` сначала
|
||||||
|
писал `Char.x`/`Char.direction`, и оба присваивания молча терялись — окно
|
||||||
|
`Char` вокруг диспетчера возвращает назад ТОЛЬКО `curr_seq` и `sword`
|
||||||
|
(`pop_savekid_state`). Направление оставалось «вправо», а все `DX`
|
||||||
|
последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево.
|
||||||
|
Вывод на будущее: геометрию персонажа в этом порте меняет `pop_map` (пишет в
|
||||||
|
`Kid`), а не диспетчер.
|
||||||
|
|
||||||
|
✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком
|
||||||
|
проёма) — недоставало второй половины `clip_char` (`obj_clip_right`) и
|
||||||
|
обрезки СПРАВА у колоночного блита. В libbgi добавлен
|
||||||
|
`gfx_blit_cols_part_w(..., maxw)`; разбор — в [`bug_closed.md`](bug_closed.md).
|
||||||
|
|
||||||
|
### L1-TRIAGE. Ревизия багов — **ЧАСТЬ 1 СДЕЛАНА 2026-08-01**
|
||||||
|
|
||||||
|
✅ **Три Critical'а прогнаны в MAME и закрыты** (протокол с числами — в
|
||||||
|
[`bug_closed.md`](bug_closed.md), раздел «Проверено в MAME 2026-08-01»):
|
||||||
|
BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2
|
||||||
|
(ping-pong при возврате) **не воспроизводятся**, BUG-3 (окклюзия climb-up на
|
||||||
|
кнопке) закрыт фиксом `tile_code_drawn` от 2026-07-28. Заодно снят неверный
|
||||||
|
диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное
|
||||||
|
поведение `goto_other_room` (`SDLPoP/src/seg002.c:390` меняет только `x`).
|
||||||
|
Список разделён на [`bug_list.md`](bug_list.md) (открытое) и
|
||||||
|
[`bug_closed.md`](bug_closed.md) (закрытое + разбор корней).
|
||||||
|
|
||||||
|
✅ **Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были
|
||||||
|
починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile`
|
||||||
|
(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty`
|
||||||
|
(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`),
|
||||||
|
разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile`
|
||||||
|
(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md).
|
||||||
|
|
||||||
|
⏳ **Осталось:** закрыть таблицу обхода 24 комнат
|
||||||
|
([`bug_list.md`](bug_list.md#обход-всех-24-комнат-уровня-1)) — заполнена на
|
||||||
|
5 строк из 24, инструмент (`ROOMNAV`) готов. Делать вместе с L1-PASS: это
|
||||||
|
единственный оставшийся источник новых багов уровня 1.
|
||||||
|
|
||||||
|
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
|
||||||
|
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
|
||||||
|
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
|
||||||
|
`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()`
|
||||||
|
= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости
|
||||||
|
эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою.
|
||||||
|
Условие «делать ПОСЛЕ CLIP-1» **снято** — CLIP-1 закрыт (см. P0). Проверка —
|
||||||
|
секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз».
|
||||||
|
|
||||||
|
### <a id="l1-pass"></a>L1-PASS. Сквозное прохождение уровня 1 — **SMOKE ПРОЙДЕН 2026-08-05**
|
||||||
|
|
||||||
|
> **Прогон 2026-08-05 (пользователь): успешный.** От старта до выхода с
|
||||||
|
> уровня одним заходом — меч подобран, **оба стража побеждены в честном бою**
|
||||||
|
> (без читов), выход отработал корректно. Это smoke: пройдены не все
|
||||||
|
> комнаты, поэтому таблица обхода 24 комнат остаётся открытой. Ценность
|
||||||
|
> прогона в том, что он снял главные риски этапа 1 разом — боёвка,
|
||||||
|
> предметы, переход с уровня — и стал регресс-базой для уровня 2.
|
||||||
|
|
||||||
|
От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота,
|
||||||
|
пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно
|
||||||
|
регресс-база для уровня 2.
|
||||||
|
|
||||||
|
Совмещать с [таблицей обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1):
|
||||||
|
известные баги закрыты, значит новые придут только отсюда. Точки, где стоит
|
||||||
|
смотреть внимательно, — уже закрытая косметика окклюзии (потолок при прыжке
|
||||||
|
вверх, шов при анимации решётки, грани дальней колонны) и подъём на
|
||||||
|
тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md).
|
||||||
|
|
||||||
|
### L2. Переход на уровень 2 и его игра — **МАШИНЕРИЯ СДЕЛАНА 2026-08-04**
|
||||||
|
|
||||||
|
Порт `levels_plan.md` §2 (шаг 1). Что появилось:
|
||||||
|
|
||||||
|
- **Номер уровня стал состоянием.** `pop_current_level` (порт
|
||||||
|
`current_level`) в `pop_level.c`; `pop_next_level` больше не флаг, а
|
||||||
|
НОМЕР — `END_LEVEL` его инкрементит (как `++next_level`, seg006:662), а
|
||||||
|
главный цикл срабатывает по расхождению `pop_next_level !=
|
||||||
|
pop_current_level` (порт `play_level_2`, seg003:0386).
|
||||||
|
- **Загрузка по номеру** — `pop_level_load_num(n)`: `LEVELS\res20NN.bin`
|
||||||
|
(fallback `a:\`), старая EMM-страница отпускается ТОЛЬКО после успешной
|
||||||
|
загрузки новой (нет файла — играем дальше на текущем). На диск кладутся
|
||||||
|
все 15 уровней (34 КБ).
|
||||||
|
- **Потабличные различия** (`data.h:840..848`) — таблицы по 16 в
|
||||||
|
`pop_level.c`: `tbl_entry_pose` (поза входа), `tbl_guard_hp` (HP стража,
|
||||||
|
ушло из хардкода `3`), `tbl_guard_type` (**−1 = стражей нет**, иначе на
|
||||||
|
14/15 они полезли бы из данных комнат), `tbl_level_type` (тайлсет —
|
||||||
|
пока только читается).
|
||||||
|
- **`find_start_level_door`** (seg003:02E6) — на уровне 2 это НЕ косметика:
|
||||||
|
стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина двери
|
||||||
|
уровня, и без `modif = 43` + `add_trob(...,3)` Кид материализуется внутри
|
||||||
|
глухой створки. Тип 3 = «быстро закрыть»: дверь захлопывается за спиной
|
||||||
|
за три кадра, как в оригинале.
|
||||||
|
- **HP через уровень** — `hitp_beg_lev` (seg003): рестарт уровня
|
||||||
|
откатывает HP к нему, пройденный уровень подтягивает его к `hitp_max`.
|
||||||
|
Заодно реализована **большая склянка** (`add_life`, тип зелья 2: +1 к
|
||||||
|
ПОТОЛКУ HP до 10) — на уровне 2 она есть, комната 20.
|
||||||
|
- **Меч** — `have_sword = level >= 2` (play_level, seg003:106), а не
|
||||||
|
жёсткий ноль.
|
||||||
|
- **Чит Shift+L** (seg000:698) — `pop_next_level = pop_current_level + 1`.
|
||||||
|
NB: `L` без Shift занят отладочным «осторожным шагом вправо»
|
||||||
|
(`pop_ctrl` `KBD_DBG_STEPR`); с Shift шаг тоже пройдёт, но уровень тут же
|
||||||
|
сменится — на практике не мешает.
|
||||||
|
|
||||||
|
**Проверено в MAME (2026-08-04):** старт уровня 1 → Shift+L → уровень 2
|
||||||
|
рисуется правильно (комната 5, большие колонны — новые для нас тайлы 8/9 —
|
||||||
|
на месте, дверь захлопнута, HP 3) → влево в комнату 4, страж на месте →
|
||||||
|
Shift+L → уровень 3 (комната 9). То есть цепочка загрузок работает
|
||||||
|
повторно, а не только один раз.
|
||||||
|
|
||||||
|
**Что осталось по уровню 2 (не машинерия, а контент):**
|
||||||
|
- сквозное прохождение уровня 2 руками — приёмка, как L1-PASS;
|
||||||
|
- выход через дверь уровня 2 (комната 23) вживую: путь тот же, что уже
|
||||||
|
закрыт на уровне 1 (L1-EXIT), но не прогнан на этом уровне;
|
||||||
|
- цвет стража из данных (`guards_color`, на уровне 2 их три разных) — у нас
|
||||||
|
один атлас; отдельная задача, к переходам отношения не имеет.
|
||||||
|
|
||||||
|
### <a id="dbg-cheats"></a>DBG-CHEATS. Отладочные читы SDLPoP — разведка сделана, код не написан
|
||||||
|
|
||||||
|
Записано 2026-08-03 (по ходу охоты за BUG-GATE-PASS-1, отложено на потом).
|
||||||
|
Мотив прямой: мост MAME **теряет нажатия при быстрой отправке**, поэтому
|
||||||
|
подогнать Кида в нужную позу для отладки автоматикой сейчас нельзя — именно
|
||||||
|
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
|
||||||
|
|
||||||
|
**Берём (дёшево и бьёт в наш класс багов):**
|
||||||
|
|
||||||
|
- **`[` / `]` — сдвинуть Кида на пиксель влево/вправо.** Оригинал —
|
||||||
|
`../SDLPoP/src/seg000.c:1828`:
|
||||||
|
```c
|
||||||
|
if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x;
|
||||||
|
else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x;
|
||||||
|
```
|
||||||
|
У нас: коды PS/2 set 2 `[` = **0x54**, `]` = **0x5B** в `pop_cheat.h`
|
||||||
|
рядом с `KBD_CHEAT_KILL/IMMO/SWORD`; обработка — в том же блоке читов
|
||||||
|
`roomtest.c` (~строка 479), **по фронту** (`*_prev`, как у остальных),
|
||||||
|
иначе одно нажатие уедет на десяток пикселей. Работает по `Kid.x`
|
||||||
|
напрямую: геометрию персонажа в этом порте меняет не диспетчер (см.
|
||||||
|
грабли L1-EXIT).
|
||||||
|
Первый же потребитель — BUG-GATE-PASS-1: там надо снять `Kid.x` и
|
||||||
|
`Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё стоит
|
||||||
|
на её тайле.
|
||||||
|
- **Shift+L — следующий уровень.** Уже входит в L2-машинерию
|
||||||
|
(`../docs/levels_plan.md` §2.4): `++pop_next_level` и всё.
|
||||||
|
|
||||||
|
**Остальные — оценка, а не обязательство:**
|
||||||
|
|
||||||
|
| Чит | Вердикт |
|
||||||
|
|-----|---------|
|
||||||
|
| **T — таймер** | **не сейчас**: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой |
|
||||||
|
| **F — остаток feather-fall** | **вместе с Shift+W**: ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно |
|
||||||
|
| **Shift+F9 — quickload с тем же уровнем** | **самое ценное и самое дорогое**: это сериализация `Char` + `room_modif` всех комнат + trob'ов + стражей (`levels_plan.md` §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки. Кандидат сразу после того, как заработают уровни |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### <a id="mem-bank5"></a>MEM-BANK5. Разгрузка W1/W2 новым банком кода
|
||||||
|
|
||||||
|
Вопрос 2026-08-04: «надо делать новый банк?». **Да, и он лечит именно то,
|
||||||
|
что жмёт.** В нашей раскладке (`MEMORY=huge`, small-вариант) CODE и DATA
|
||||||
|
живут в ОДНОМ 32-КБ пространстве W1+W2 — карта текущей сборки:
|
||||||
|
|
||||||
|
```
|
||||||
|
_CODE 0x4100..0xAA00 26880 Б
|
||||||
|
_DATA 0xAAD0..0xB9B0 3808 Б
|
||||||
|
_BSS 0xB9B8..0xBADA 290 Б
|
||||||
|
куча 0xBADA..0xBB00 38 Б ← упёрлись сюда, добавляя отладку
|
||||||
|
стек 0xBB00..0xC000 1280 Б
|
||||||
|
```
|
||||||
|
|
||||||
|
Поэтому **каждый килобайт кода, уехавший в банк, становится килобайтом,
|
||||||
|
доступным данным**. Отдельного «дефицита W2» у нас нет — дефицит один.
|
||||||
|
(38 байт кучи не опасны сами по себе: malloc'ом мы не пользуемся, страницы
|
||||||
|
берутся через `mem_alloc_block`. Опасно то, что следующая структура
|
||||||
|
данных упрётся в стек молча.)
|
||||||
|
|
||||||
|
Резидентный код по модулям (из `.sprinter-cc-roomtest/*.rel`):
|
||||||
|
|
||||||
|
| модуль | _CODE | как часто зовётся | в банк? |
|
||||||
|
|--------|------:|-------------------|---------|
|
||||||
|
| `pop_kid.c` | 6287 | `load_frame`/`play_seq` — 2×/кадр (Кид + страж) | частично: холодная половина (загрузка страниц спрайтов, `pop_kid_load`) — да; движок кадров — нет |
|
||||||
|
| `roomtest.c` | 3893 | main-loop | нет (точка входа, зовёт всех) |
|
||||||
|
| `pop_trob.c` | 2526 | `do_trobs` 1×/кадр, но `pop_trob_modif` — горячий аксессор | **кандидат №1**, если вынести аксессор в резидент |
|
||||||
|
| `pop_level.c`| 2292 | `pop_level_tile` — из `pop_bg` (банк 2) на каждый тайл | **нет**: банк→банк на каждый тайл убьёт отрисовку |
|
||||||
|
| `pop_ctrl.c` | 2189 | `user_control` 1×/кадр | **кандидат №2** — дёшево и безопасно |
|
||||||
|
| `pop_guard.c`| 923 | 1×/кадр | нет смысла |
|
||||||
|
|
||||||
|
Порядок действий, когда упрёмся: `pop_ctrl.c` → банк 5 (2.2 КБ, один
|
||||||
|
banked-вызов за кадр), затем расщепление `pop_kid.c` на горячее ядро и
|
||||||
|
холодную загрузку. Критерий кандидата — **не размер, а частота вызова и
|
||||||
|
отсутствие горячих банк→банк переходов**; `pop_level` показывает, что
|
||||||
|
большой холодный на вид модуль может быть горячим аксессором.
|
||||||
|
|
||||||
|
Брать по факту нехватки места, не заранее.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Отложено осознанно (не брать, пока не появится причина)
|
||||||
|
|
||||||
|
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
|
||||||
|
- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6.
|
||||||
|
- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти
|
||||||
|
бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается.
|
||||||
|
- **Отключение мыши на время игры** и **замена PRNG** —
|
||||||
|
`../docs/ideas_backlog.md` (оба дают доли процента кадра).
|
||||||
|
- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость
|
||||||
|
транзиентная; разбор в [`bug_closed.md`](bug_closed.md).
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* _pop_draw.h — внутренний заголовок слоя отрисовки: выбор ядра libbgi
|
||||||
|
* (клипающее / линейное «noclip») по одному и тому же тесту для блита и
|
||||||
|
* для heal.
|
||||||
|
*
|
||||||
|
* ПОЧЕМУ ОТДЕЛЬНЫЙ ФАЙЛ, А НЕ pop_bg.h. Обе функции — `static inline`, а
|
||||||
|
* SDCC 4.5 оставляет тело такой функции в КАЖДОМ TU, который видит
|
||||||
|
* объявление, даже если тот её не зовёт (memory
|
||||||
|
* `sdcc_inline_codegen_findings`). pop_bg.h включают почти все модули PoP,
|
||||||
|
* то есть из общего заголовка эти ~100 Б размножились бы десятком мёртвых
|
||||||
|
* копий (замер CLIP-1, 2026-08-01: +1091 Б в _CODE и +636 Б в банке
|
||||||
|
* стража — за код, который там никто не вызывает). Здесь их видят ровно
|
||||||
|
* три файла, которые реально рисуют: pop_bg.c, pop_kid.c, pop_gdraw.c.
|
||||||
|
*/
|
||||||
|
#ifndef POP_DRAW_H
|
||||||
|
#define POP_DRAW_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
|
||||||
|
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
|
||||||
|
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
|
||||||
|
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
|
||||||
|
* того, вылезает край или нет. */
|
||||||
|
static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h)
|
||||||
|
{
|
||||||
|
return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 &&
|
||||||
|
x + (int)w <= 320 && y + (int)h <= 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стереть прямоугольник (heal из ОЗУ-копии) тем же приёмом, что и блит:
|
||||||
|
* целиком на экране → линейное ядро без клипа. Условие входа у
|
||||||
|
* gfx_heal_noclip ровно то же, что у gfx_blit_cols_part_noclip, поэтому
|
||||||
|
* тест один — общее ядро gfx_heal платит за клип и 16-бит, а не за пиксели
|
||||||
|
* (замер: 11 658 тактов на heal 22×22).
|
||||||
|
*
|
||||||
|
* НЕ inline (тело — pop_draw.c, резидент W1): SDCC 4.5 встраивал бы его в
|
||||||
|
* каждое место вызова по 181 Б И оставлял мёртвую копию в каждом TU — см.
|
||||||
|
* шапку pop_draw.c с замером. Зовётся 5 раз за кадр, цена вызова тонет в
|
||||||
|
* стоимости самого heal.
|
||||||
|
*
|
||||||
|
* w/h тут int, а не uint16_t: вызывающие считают их вычитанием (h − skip), и
|
||||||
|
* отрицательный результат обязан быть no-op, а не превратиться в огромный
|
||||||
|
* unsigned. */
|
||||||
|
void pop_heal_fast(int x, int y, int w, int h);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,363 +1,281 @@
|
|||||||
# roomtest — список известных багов (Фаза B и смежное)
|
# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
|
||||||
|
|
||||||
Найдено при тестировании Фазы B (кнопки/ворота) в MAME. **НЕ решаем сейчас**
|
Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней)
|
||||||
— задача следующего этапа. Правило проекта: механику сверять с
|
переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
|
||||||
`applications/PoP/SDLPoP/src/` ДО кодинга.
|
грепни там по симптому.
|
||||||
|
|
||||||
Статус на момент записи: 2026-07-21. Тестовый старт — комната 6 (`START_ROOM`
|
Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта:
|
||||||
в `roomtest.c`). Кнопка room6(0,2, opener) → ворота room8(0,9), видны в левом
|
механику сверять с `../SDLPoP/src/` ДО кодинга.
|
||||||
шве room6.
|
|
||||||
|
Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1). Одиннадцать
|
||||||
|
наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены
|
||||||
|
в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень
|
||||||
|
стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень),
|
||||||
|
BUG-DEATH-1 (смерть от меча), BUG-GATE-ANIM-1 (анимация ворот), BUG-COLL-1
|
||||||
|
(полный порт `check_collisions`), BUG-STANDUP-1 (лишний guard в
|
||||||
|
`bumped_floor`). Клавиатурный BUG-KBD-3 доведён до конца во второй волне
|
||||||
|
(см. ниже); из первой волны открытым остался косметический BUG-SEAM-DRAW-1.
|
||||||
|
|
||||||
|
Ревизия **2026-08-03, вечер** (вторая волна с прогона): все четыре наблюдения
|
||||||
|
закрыты — BUG-KBD-4 (фантомный отжим Shift), BUG-RESPAWN-2 (стражи не оживают),
|
||||||
|
BUG-DRAWORDER-1 (Кид поверх тела стража) и BUG-LOOSE-2 (осколки только от одной
|
||||||
|
из двух плит). У BUG-DRAWORDER-1 остался артефакт «ноги поверх головы»,
|
||||||
|
сверенный с SDLPoP и записанный в «НЕ БАГИ». Тогда же закрыт как
|
||||||
|
невоспроизводящийся BUG-SEAM-DRAW-1 (Кид в шве рисуется) и заведён
|
||||||
|
BUG-GATE-PASS-1 — однократное наблюдение прохода сквозь закрывшуюся решётку,
|
||||||
|
которое ЖДЁТ надёжного сценария.
|
||||||
|
|
||||||
|
| ID | что | тип | статус |
|
||||||
|
|----|-----|-----|--------|
|
||||||
|
| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу |
|
||||||
|
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария** (BUG-GATEMOD-1, из-за которого решётка стартовала не в том состоянии, закрыт — перепроверить) |
|
||||||
|
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
|
||||||
|
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
|
||||||
|
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |
|
||||||
|
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — **Critical**
|
<a id="уровень-2"></a>
|
||||||
|
# Уровень 2 — баги с приёмки (заведено 2026-08-04)
|
||||||
|
|
||||||
**Симптом:** при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов)
|
Уровень 2 заиграл 2026-08-04 (см. [`TASKS.md`](TASKS.md) запись **L2**).
|
||||||
Kid оказывается на ряду **row 1** вместо row 0 — «проваливается» на этаж ниже
|
Пользователь при первом прогоне увидел **несколько багов отрисовки** и
|
||||||
(стоит в дверном проёме на неправильной высоте).
|
подаст их списком отдельно — раздел заведён под них заранее, чтобы
|
||||||
|
наблюдения не растеклись по переписке.
|
||||||
|
|
||||||
**Где:** `pop_map.c` `check_leave()` (порт seg002 leave_room) — боковой уход
|
**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md#bug-loose-3) — чёрный
|
||||||
`pop_leave_dir`; репроекция координат `Kid.x ∓= 140` есть, но Y/`curr_row`
|
бар под упавшей плитой-потолком (запечка восстанавливала не всё, что стёрла).
|
||||||
при боковом переходе НЕ репроецируются (в отличие от `check_leave_below`,
|
|
||||||
где `Kid.y -= 189; curr_row = y_to_row`). При входе в соседнюю комнату у
|
|
||||||
шва Kid, видимо, попадает в позицию, где `determine_col`/физика следующего
|
|
||||||
кадра сбрасывают его на row 1 (нет пола на row 0 у входа, либо неверный y).
|
|
||||||
|
|
||||||
**Сверить с SDLPoP:** `goto_other_room` (seg002) — как именно
|
**Порядок работы по уровню 2 (2026-08-05):** наблюдения СНАЧАЛА копятся
|
||||||
пересчитываются `Char.curr_row`/`Char.y` при side-переходе (left/right).
|
здесь целиком — фиксы берём, когда уровень пройден до конца. Причина:
|
||||||
Для вертикального перехода мы это сделали, для бокового — нет.
|
косметика отрисовки почти всегда сводится к общему корню (ср. волну
|
||||||
|
BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит
|
||||||
|
чинить один и тот же корень трижды.
|
||||||
|
|
||||||
## BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — **Critical**
|
Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие
|
||||||
|
кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass),
|
||||||
|
запись **L2-PASS**: по ней видно, «механика не сработала» это или
|
||||||
|
«так и задумано».
|
||||||
|
|
||||||
**Симптом:** после перехода в соседнюю комнату попытка сразу вернуться
|
Перед заведением записи — грепнуть [`bug_closed.md`](bug_closed.md) по
|
||||||
обратно приводит к тому, что Kid снова закидывается в ту же комнату —
|
симптому: почти вся косметика окклюзии уровня 1 уже разбиралась
|
||||||
переход «пинг-понгует», выйти нельзя.
|
(BUG-CEIL-1/2/3, BUG-OCCL-1, BUG-DOOR-CLIP, BUG-DRAWORDER-1), и уровень 2
|
||||||
|
вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9)
|
||||||
|
и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый.
|
||||||
|
|
||||||
**Где:** `pop_map.c` `check_leave()`. После перехода Kid репроецирован к
|
<a id="уровень-3"></a>
|
||||||
ПРОТИВОПОЛОЖНОЙ кромке новой комнаты (`x ∓ 140`). На СЛЕДУЮЩЕМ же кадре
|
# Уровень 3 — баги с приёмки (заведено 2026-08-05)
|
||||||
передний край снова за порогом ухода в обратную сторону → `pop_leave_dir`
|
|
||||||
взводится опять → мгновенный обратный переход. Нужен guard «только что
|
|
||||||
перешёл» (подавить повторный leave на N кадров / пока Kid не отойдёт от
|
|
||||||
кромки), как в оригинале (`next_room`/leave обрабатываются раз за кадр +
|
|
||||||
Kid входит достаточно внутрь).
|
|
||||||
|
|
||||||
**Сверить с SDLPoP:** порядок `leave_room`/`goto_other_room` + на какой
|
Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два
|
||||||
X ставится персонаж при входе (должен быть НЕ на самой кромке, иначе
|
из них — **не баги, а неначатые задачи**, и записаны как таковые:
|
||||||
мгновенный обратный триггер).
|
|
||||||
|
|
||||||
## BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия (Kid поверх кнопки) — **Critical**
|
| наблюдение | что это на самом деле |
|
||||||
|
|------------|------------------------|
|
||||||
|
| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта |
|
||||||
|
| к.10: скелет не оживает | [L3-SKEL](TASKS.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован. **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 |
|
||||||
|
|
||||||
**Симптом:** когда Kid подтягивается (climb-up) на тайл, верх которого —
|
Остальные два были настоящими багами и **закрыты в тот же день** — разбор
|
||||||
КНОПКА (opener/closer), а не обычный пол, отрисовка неверная: Kid рисуется
|
корней в [`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** (флаги
|
||||||
ПОВЕРХ кнопки, хотя передняя грань пола/кнопки должна перекрывать нижнюю
|
коллизий считались для одного ряда вместо трёх — бамп терялся на кадре
|
||||||
часть Kid во время подъёма.
|
смены ряда) и **BUG-SEAM-WEDGE-1** (сосед угла (2,0) безусловно считался
|
||||||
|
стеной).
|
||||||
|
|
||||||
**Где:** `pop_bg.c` `climb_overlay_tile()` — floor-overlay при подъёме
|
<a id="bug-spike-1"></a>
|
||||||
рисуется только для floor-подобных тайлов `code ∈ {1,3,5,19}`. Кнопка
|
## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
|
||||||
(opener 0x0F / closer 0x06) в этот набор НЕ входит → пол-оверлей кнопки не
|
|
||||||
рисуется поверх Kid. Нужно: для нажатой кнопки (draw как floor/stuck)
|
|
||||||
трактовать её как floor-тайл в climb-overlay (учесть подстановку из
|
|
||||||
`get_tile_to_draw`), и/или добавить opener/closer в набор.
|
|
||||||
|
|
||||||
**Сверить с SDLPoP:** `draw_floor_overlay` (seg008) + `get_tile_to_draw`
|
> **Статус 2026-08-05, вечер.** Пользователь **повторить не смог**, а замер
|
||||||
(нажатая кнопка → tiles_1_floor / tiles_5_stuck) — оверлей должен идти по
|
> (ниже) показал, что чистый пробег по убранным пикам убивает штатно. То
|
||||||
ПОДСТАВЛЕННОМУ типу тайла.
|
> есть в смертельности бага, похоже, нет вовсе: наблюдался частный случай —
|
||||||
|
> пики, УЖЕ выдвинутые полностью (h = 1), для бегущего безвредны и в
|
||||||
|
> оригинале. Запись оставлена открытой ровно из-за визуального расхождения
|
||||||
|
> со скриншотом SDLPoP (у нас острия торчат, у него убраны) — см. конец.
|
||||||
|
>
|
||||||
|
> **Как получить состояние нарочно:** подойти к пикам вплотную (выдвинутся),
|
||||||
|
> отступить на полшага, НЕ выходя из зоны срабатывания, и пробежать по ним.
|
||||||
|
> Пользователь пробовал и это, и попиксельную подгонку X читом `[`/`]` —
|
||||||
|
> не поднялось. Вывод для будущего разбора: состояние **не чисто
|
||||||
|
> позиционное**, одной шириной габарита его не объяснить; следующий
|
||||||
|
> подозреваемый — момент, в который `process_trobs` застаёт модификатор
|
||||||
|
> относительно кадра Кида.
|
||||||
|
|
||||||
|
**Наблюдение (пользователь, 2026-08-05).** Уровень 2, комната 6, пики (1,3):
|
||||||
|
|
||||||
|
| действие | что происходит |
|
||||||
|
|----------|----------------|
|
||||||
|
| длинный прыжок с ряда 0 на пики | **смерть — правильно** (путь `fell_on_spikes`) |
|
||||||
|
| пробег по ряду 1 прямо по пикам | **урона нет** |
|
||||||
|
| после уборки пик | **на экране остаются белые остатки остриёв** (в оригинале чисто) |
|
||||||
|
| прыжок на месте, стоя на пиках | урона нет |
|
||||||
|
| просто стоять на выдвинутых пиках | можно сколько угодно |
|
||||||
|
|
||||||
|
**Замер (MAME, чтение `room_modif` комнаты 6).** Пока Кид стоит на тайле,
|
||||||
|
модификатор пики (индекс 13) = **0x8E**, то есть «пики ПОЛНОСТЬЮ вышли и
|
||||||
|
идёт обратный отсчёт». Дальше вся арифметика сходится с оригиналом:
|
||||||
|
|
||||||
|
```
|
||||||
|
is_spike_harmful (seg007:1178): 0/-1 → 0; <0 → 1; 1..4 → 2; >=5 → 0
|
||||||
|
check_spiked (seg006:0658): убивает при h>=2 на кадрах бега 7..14
|
||||||
|
и при h!=0 на кадрах приземления 43/26
|
||||||
|
```
|
||||||
|
|
||||||
|
То есть **при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале** —
|
||||||
|
смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2). Наши
|
||||||
|
`animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked`
|
||||||
|
сверены с seg006/seg007 построчно и совпадают дословно.
|
||||||
|
|
||||||
|
**Значит расходится ТАЙМИНГ, а не логика:** у нас пики успевают полностью
|
||||||
|
выйти ДО того, как Кид на них вбежит, и он проскакивает мимо смертельного
|
||||||
|
окна 1..4. Триггер — `check_spike_below` (seg006:1199), и у нас он берёт
|
||||||
|
габарит спрайта из `kid_fp` — метрик ПОСЛЕДНЕГО отрисованного кадра, а
|
||||||
|
оригинал берёт `char_x_left/right` ТЕКУЩЕГО кадра (`set_char_collision`).
|
||||||
|
Это первый подозреваемый; второй — момент вызова `process_trobs`
|
||||||
|
относительно `play_frame` (сколько кадров модификатор успевает пройти,
|
||||||
|
пока Кид добегает).
|
||||||
|
|
||||||
|
**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом) — оно и уточняет
|
||||||
|
гипотезу.** Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и
|
||||||
|
«спрятались не все — часть артефактов осталась». Скриншоты рядом:
|
||||||
|
[наш](bugscreens/l2-r6-spikes-ours.png) и
|
||||||
|
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под
|
||||||
|
щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.
|
||||||
|
|
||||||
|
Это стыкуется с замером: пока Кид стоит рядом, `check_spike_below` каждый
|
||||||
|
кадр зовёт `start_anim_spike`, а тот при отрицательном модификаторе
|
||||||
|
переставляет его обратно в 0x8F — то есть пики у нас «залипают выдвинутыми»
|
||||||
|
и отсчёт до уборки не доходит. В оригинале в той же позе они убраны,
|
||||||
|
значит его `check_spike_below` эту колонку УЖЕ не задевает.
|
||||||
|
|
||||||
|
**ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер
|
||||||
|
2026-08-05, MAME, watchpoint на `room_modif[13]` комнаты 6).** Чистый
|
||||||
|
пробег по УБРАННЫМ пикам убивает штатно:
|
||||||
|
|
||||||
|
```
|
||||||
|
запись modif=1 : кадр 11 (беговой), x=112, curr_col=2 ← пики пошли вверх
|
||||||
|
запись modif=2 : кадр 12 (беговой), x=117, curr_col=3 ← Кид уже НА тайле, h=2
|
||||||
|
запись modif=3 : кадр 177 (frame_177_spiked) ← напоролся
|
||||||
|
```
|
||||||
|
|
||||||
|
То есть `check_spike_below`, `check_spiked`, `is_spike_harmful` и тайминг
|
||||||
|
выдвижения работают правильно, и «раннего» триггера нет.
|
||||||
|
|
||||||
|
**Настоящий корень — пики ЗАЛИПАЮТ выдвинутыми.** Пока габарит Кида
|
||||||
|
накрывает колонку пики, `check_spike_below` каждый кадр зовёт
|
||||||
|
`start_anim_spike`, а тот при отрицательном модификаторе переставляет его
|
||||||
|
обратно в 0x8F — отсчёт до уборки не доходит. А выдвинутые пики (h = 1)
|
||||||
|
для бегущего БЕЗВРЕДНЫ по правилам оригинала. Отсюда обе жалобы: пробег
|
||||||
|
по уже вышедшим пикам не убивает, и они же остаются торчать на экране.
|
||||||
|
|
||||||
|
**Что осталось выяснить (и это единственный открытый вопрос).** Код
|
||||||
|
`start_anim_spike` у нас с оригиналом совпадает дословно, значит оригинал
|
||||||
|
тоже удерживал бы пики, стой Кид там же. На скриншоте SDLPoP в похожей
|
||||||
|
позе пики УБРАНЫ — то есть его Кид стоит чуть левее и его габарит колонку
|
||||||
|
пики уже не задевает. Разница в 2–3 пикселя посадки, а у нас такие
|
||||||
|
расхождения по X уже ловились (см. заметку в BUG-GRAB-1: после касания
|
||||||
|
площадки SDLPoP уводит Кида на 134, мы — на 141).
|
||||||
|
|
||||||
|
**Как закрывать:** инструментировать SDLPoP (печать `char_x_left/right`,
|
||||||
|
`left/right_checked_col` и модификатора пики каждый кадр), проиграть ту же
|
||||||
|
сцену — падение плиты-потолка в комнате 6 и остановку на щебне — и сверить
|
||||||
|
с нашей трассой ПОЗИЦИЮ КИДА после приземления. Если позиции совпадут, а
|
||||||
|
диапазоны колонок разойдутся — виноват габарит (`kid_fp` против
|
||||||
|
`set_char_collision` текущего кадра); если разойдутся позиции — это отдельный
|
||||||
|
баг посадки, а пики — его следствие.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Оптимизации (отложено)
|
|
||||||
|
|
||||||
### OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22
|
<a id="ручная-перепроверка-2026-08-03"></a>
|
||||||
|
# Ручная перепроверка фиксов (2026-08-03)
|
||||||
|
|
||||||
**Возможность:** `pop_room_redraw_seam_left()` (pop_bg.c) на каждое изменение
|
Шесть корней закрыты автоматической проверкой в MAME (мост `mame-z80`:
|
||||||
openness рисует `bar(BLACK)` по всему col0 + **полный `draw_tile(0,0)`**
|
чтение `_Kid`, потиковые трассы, скриншоты) — этого достаточно, чтобы
|
||||||
(стены, `topright`, `wall_pattern` с `prandom()` — десятки блитов). Реально
|
показать, что конкретный сценарий больше не воспроизводится, но НЕ
|
||||||
анимируются только бары решётки — `draw_gate_back` (~9 `env_b`).
|
достаточно, чтобы поймать регрессии в соседней механике. Ниже — сценарии
|
||||||
|
ровно в тех формулировках, в которых баги были заведены.
|
||||||
|
|
||||||
**Стоимость:** seam-блок (синий io_border) занимает ~30-50% кадрового периода,
|
Собрано и лежит в образе (`make` + `make hdd` уже прогнаны; libc пересобран,
|
||||||
**но только пока openness меняется** — т.е. во время открытия и медленного
|
`make size-check` без роста).
|
||||||
авто-закрытия (~5 сек после схода с кнопки). В покое (ворота открыты-держим
|
|
||||||
кнопку / закрыты / FF) — 0%. Замерено в MAME: брейк на
|
|
||||||
`_pop_room_redraw_seam_left` (0x5A54) срабатывает ⟺ сегмент дорогой.
|
|
||||||
|
|
||||||
**Приём** (как у пик, но сложнее — heal НЕ годится: печёные бары устаревшие,
|
| # | что проверить | ожидаемо |
|
||||||
поэтому и стоит `bar(BLACK)`+redraw): `bar(BLACK)` только по полосе баров
|
|---|---------------|----------|
|
||||||
(x=0, gate_top..gate_bot) + `draw_gate_back(lmod,...)` + дорисовать статику
|
| 1 | комната 22: выпить зелье (0,6), выйти в 16/23 и вернуться | кувшина нет, пузырька над пустым местом нет |
|
||||||
тайла (0,0), задетую полосой. Ожидаемо срезает бóльшую часть 30-50%.
|
| 2 | то же для комнат 14 (0,5) и 17 (2,3) | так же |
|
||||||
**Пиксель-чувствительно** — полоса баров пересекается со статикой col0,
|
| 3 | комната 15: подобрать меч (2,2), выйти и вернуться | меча нет; кладка на дальней стене НЕ мигает |
|
||||||
обязательна выверка в MAME по кромкам.
|
| 4 | комната 12: разбить плиты (0,1)/(0,2) и потолок в 16, выйти-вернуться | остаются разбитыми, проём не закрывается |
|
||||||
|
| 5 | комната 17 из 23: разбить (1,5)/(1,6), выпить зелье (2,3), вернуться | всё остаётся |
|
||||||
|
| 6 | **после смерти** зайти в те же комнаты | ВСЁ восстановлено (в оригинале смерть = `load_level`) |
|
||||||
|
| 7 | комната 12: разбег в закрытую решётку (0,9) с полушага | не проходит насквозь; перелистывание экрана штатно (см. BUG-SEAM-DRAW-1 в `bug_closed.md`) |
|
||||||
|
| 8 | комната 6: бег справа налево от (0,9), длинный прыжок (0,6)→(0,7) | не влетает внутрь стены |
|
||||||
|
| 9 | комната 5: с кнопки (0,6) падение на (2,7), присед, вставание | остаётся в комнате 5 (проверено трассой: `fr=111 x=177` → `seq_47` → `fr=15 x=173`) |
|
||||||
|
| 10 | комната 5: нажать кнопку (0,4) | поднимаются ОБЕ решётки — (0,5) видно на экране, (0,9) проверять из комнаты 1 |
|
||||||
|
| 11 | страж (комнаты 3, 21) убивает Кида | смерть доигрывается, респавн в стартовой позиции уровня; цикла «убил-воскрес» нет |
|
||||||
|
| 12 | клавиатура: долгая игра с Shift+стрелка | ↑ и Shift не залипают; осторожный шаг не превращается в бег (доработано во второй волне — п.17) |
|
||||||
|
|
||||||
**Решение (2026-07-22):** оставляем как есть — стоимость транзиентная, в бюджет
|
## Вторая волна (2026-08-03, вечер)
|
||||||
помещаемся. Делать, если упрёмся в кадровый бюджет на сценах с воротами.
|
|
||||||
|
| # | что проверить | ожидаемо |
|
||||||
|
|---|---------------|----------|
|
||||||
|
| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки |
|
||||||
|
| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные |
|
||||||
|
|
||||||
|
Отдельно стоит посмотреть **на регрессии от порта `check_collisions`** — он
|
||||||
|
трогает всю горизонтальную коллизию: бамп в стену на бегу и в прыжке,
|
||||||
|
осторожный шаг у стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме
|
||||||
|
решётки, переходы через швы (старый BUG-SEAM-PINGPONG).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Исправлено
|
# Открытые баги уровня 1
|
||||||
|
|
||||||
- **Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука
|
<a id="bug-gate-pass-1"></a>
|
||||||
срезана до одного пикселя** — ИСПРАВЛЕНО 2026-07-28. Две причины:
|
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ
|
||||||
(а) не был портирован `clip_char()` (seg006:1749) — верхняя обрезка спрайта по
|
|
||||||
`y_clip[curr_row+1]`, когда тайл над головой стена/пол; сделано
|
|
||||||
(`pop_clip_char_top` в pop_map.c + `gfx_blit_cols_part` в libbgi, heal чистит
|
|
||||||
уже обрезанный прямоугольник);
|
|
||||||
(б) `climb_overlay_tile` выбирал ветку `draw_floor_overlay` (seg008:1E3A) по
|
|
||||||
СЫРОМУ коду тайла — а нажатая кнопка в `get_tile_to_draw` (seg008:240)
|
|
||||||
подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в
|
|
||||||
`draw_other_overlay` и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки
|
|
||||||
`floor_left_overlay[frame-137]`. Фикс — `tile_code_drawn()` (одна подстановка
|
|
||||||
на все слои). Проверено покадрово в MAME (кадры 148..138).
|
|
||||||
|
|
||||||
- **Шов ворот жёг 50% кадра в покое (закрытая решётка)** — ИСПРАВЛЕНО
|
**Статус: наблюдался один раз, воспроизвести повторно НЕ удалось.** Заведён,
|
||||||
2026-07-22. Причина — **баг кодогенератора SDCC z80** (memory
|
чтобы наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
|
||||||
`sdcc_z80_cmp_store_a_bug`): `if (m[9] != seam_sig) seam_sig = m[9];`
|
«не баг», пока нет надёжного сценария.
|
||||||
компилировался в `sub (seam_sig)` (A ← разность) + `ld (seam_sig),a` —
|
|
||||||
сохранял РАЗНОСТЬ `m[9]-seam_sig`, не `m[9]`. `seam_sig` осциллировала
|
|
||||||
(напр. 25↔231), `m[9]!=seam_sig` истинно каждый кадр → `draw_tile(0,0)`
|
|
||||||
каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения
|
|
||||||
(`seam_sig = g;` из чистого `g` ДО `sub`), подтверждён в .asm. Прочёс всех
|
|
||||||
модулей PoP: других случайных compare-then-store нет.
|
|
||||||
|
|
||||||
- **Пики: 2×полный `draw_tile` на кадр анимации** → хирургический редрой,
|
**Что наблюдалось (2026-08-03).** Комната 5: Кид стоял НА тайле решётки (0,9)
|
||||||
2026-07-22. `pop_spike_redraw`: `heal_off` уже возвращает всю печёную
|
и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1
|
||||||
статику (база 127, пол, грани); поверх анимируются только два острия
|
и упал на (1,1).
|
||||||
(`SPIKES_FRAM_LEFT` в своей ячейке + `SPIKES_FRAM_RIGHT` в соседней).
|
|
||||||
Заменили 2×`draw_tile` на 2×`env_b` — пиксель-в-пиксель тот же результат,
|
|
||||||
в разы дешевле (шахта из нескольких пик больше не съедает полный кадр).
|
|
||||||
|
|
||||||
- **Отрисовка нажатой кнопки (0,2)/(0,3)** — ИСПРАВЛЕНО. Причина: `fore_tile`
|
**Что уже измерено и в чём загвоздка.** Сразу после наблюдения повторить не
|
||||||
(pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id
|
получилось: в том же месте Кид стоит на `x = 196`, `col = 9`, и решётка его
|
||||||
149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки».
|
ДЕРЖИТ — то есть штатно.
|
||||||
Фикс: `fore_tile` применяет ту же подстановку нажатой кнопки, что и
|
|
||||||
`draw_tile` (opener→floor / closer→stuck при таймере связи >1). Плюс
|
|
||||||
`pop_button_redraw` — wipe своей ячейки + правой грани (дальний угол в
|
|
||||||
0,3), низ строго yb+64 (не залезать в стену ряда 1).
|
|
||||||
|
|
||||||
## НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)
|
Арифметика оригинала объясняет разницу. `is_obstacle` (seg004) ставит
|
||||||
|
плоскость блокировки в `x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX`, для
|
||||||
|
колонки 9 это **x = 205**. При этом «колонка 9» по `get_tile_div_mod_m7` —
|
||||||
|
это `x ∈ [191, 205)`. Пока `curr_col == 9`, Кид гарантированно левее
|
||||||
|
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться **правее
|
||||||
|
205**, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
|
||||||
|
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
|
||||||
|
весь тайл.
|
||||||
|
|
||||||
- **Голова стоящего Кида поверх падающей на него loose-плиты.** Комната 12:
|
Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
|
||||||
зацеп не удался, Кид остался стоять, сбитая плита падает прямо на него —
|
по тайлу дальше, пока решётка была поднята), и поведение штатное. Но повторить
|
||||||
голова рисуется ПОВЕРХ плиты. Сверено покадрово с SDLPoP v1.24 (2026-07-29):
|
эту позу и снять `x` пока не удалось, поэтому гипотеза НЕ подтверждена.
|
||||||
там ровно то же самое. Артефакт оригинального движка (порядок midtable), а
|
|
||||||
не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев,
|
|
||||||
которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и
|
|
||||||
падение вместе с плитой — там плита обязана быть поверх Кида.
|
|
||||||
|
|
||||||
## Заметки
|
**Что снять в следующий раз** (без этих чисел вопрос не закрыть):
|
||||||
|
|
||||||
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
|
1. `Kid.x` и `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё
|
||||||
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
|
стоит на её тайле — до шага вправо;
|
||||||
сторону = разворот (как в оригинале safe_step), движение со второго.
|
2. модификатор решётки (openness) комнаты 5, тайл 9 — `can_bump_into_gate()`
|
||||||
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`).
|
считает её препятствием только пока `(modif >> 2) + 6 < char_height`, то
|
||||||
- `START_ROOM=6` — временный тестовый старт; вернуть на
|
есть пока она опустилась достаточно низко относительно РОСТА кадра;
|
||||||
`pop_level_start_room()` после отладки объектов.
|
3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.
|
||||||
|
|
||||||
---
|
Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
|
||||||
|
продолжить), затем чтение `_Kid` из отладчика MAME.
|
||||||
|
|
||||||
## BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **Medium**
|
**Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке,
|
||||||
|
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
|
||||||
|
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
|
||||||
|
(ср. закрытый BUG-SEAM-PINGPONG). Второй кандидат — `char_height` в
|
||||||
|
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может
|
||||||
|
перестать считаться препятствием раньше времени.
|
||||||
|
|
||||||
**Симптом (2026-07-22):** при прыжке вверх (SEQ up, кадры 67..79) руки/голова
|
# Оптимизация отрисовки (записано 2026-07-29)
|
||||||
Kid заходят в полосу кладки у потолка (row -1) и рисуются ПОВЕРХ неё — потолок
|
|
||||||
должен окклюдить руки (как передняя грань).
|
|
||||||
|
|
||||||
**Где:** `pop_bg.c` — полоса потолка (`draw_tile(-1)`) сейчас только фон
|
|
||||||
(запечена в pop_room_draw), не окклюдер поверх Kid. Нужен ceiling-overlay в
|
|
||||||
`pop_fore_over_kid` (перерисовать срез полосы над Kid поверх спрайта), по
|
|
||||||
аналогии с fore-тайлами. Инфраструктура `g_clip_top`/`gfx_blit_part` уже есть.
|
|
||||||
|
|
||||||
## BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row -1) не работает — **Medium**
|
|
||||||
|
|
||||||
**Симптом (2026-07-22):** loose-плита в ряду 2 верхнего соседа (room5 (2,5) →
|
|
||||||
потолок room6 над (0,5)) при прыжках Kid на (0,5) НЕ трясётся и не разбивается.
|
|
||||||
|
|
||||||
**Где:** верхний сосед не «живой» — `above_fg` берётся из статики уровня,
|
|
||||||
loose-состояние соседней комнаты не тянем (per-room loose/modif не персистятся,
|
|
||||||
P0). check_press/knock работают только по текущей комнате. Нужно: (а) пробой
|
|
||||||
loose сверху через границу комнаты, (б) персистентное loose-состояние соседа +
|
|
||||||
его отрисовка в полосе. Пока полоса рисует loose статически (кадр покоя).
|
|
||||||
|
|
||||||
## BUG-OCCL-1. Тень дальней колонны перекрывает Kid в (0,4)-(0,5) — **Medium**
|
|
||||||
|
|
||||||
**Симптом (2026-07-22):** Kid, стоя/прыгая у (0,4)-(0,5), частично перекрыт
|
|
||||||
тёмной штриховкой — это ТЕНЬ/боковая грань ДАЛЬНЕЙ колонны (сзади по глубине),
|
|
||||||
которая НЕ должна окклюдить персонажа. Over-occlusion fore-слоя.
|
|
||||||
|
|
||||||
**Где:** `pop_bg.c` `pop_fore_over_kid`/`fore_tile` — рисует fore футпринт-тайлов
|
|
||||||
без учёта глубины (какая грань РЕАЛЬНО перед Kid). Порт seg003 redraw_at_char
|
|
||||||
окклюдит только передние грани; дальняя колонна (за Kid) не должна. Сверить с
|
|
||||||
SDLPoP redraw_at_char (какие тайлы/грани идут в fore vs back по y-сортировке).
|
|
||||||
|
|
||||||
## BUG-CEIL-3. Анимация ворот стирает потолок над ними — **Medium**
|
|
||||||
|
|
||||||
**Симптом (2026-07-22):** при опускании/поднятии решётки шва (анимация openness)
|
|
||||||
полоса кладки у потолка НАД воротами пропадает (см. потолок над левым швом).
|
|
||||||
|
|
||||||
**Где:** `pop_bg.c` `pop_room_redraw_seam_left` (и `pop_button_redraw`/
|
|
||||||
`pop_spike_redraw` — общий класс): чёрный `bar` по col0 row0 стирает потолочную
|
|
||||||
полосу (row-1), а redraw рисует только сам гейт, не восстанавливая полосу.
|
|
||||||
Нужно: при перерисовке шва/тайла у потолка перерисовать и срез потолочной полосы
|
|
||||||
(draw_tile(-1,col) с g_clip_top) над затронутыми колонками.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22**
|
|
||||||
|
|
||||||
Статус: 2026-07-22, вечер. НАСТОЯЩИЙ корень найден потиковой трассой ЖИВОГО
|
|
||||||
SDLPoP 1.23 (lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char +
|
|
||||||
char_x_left/right; fixes выключены = vanilla). Все прежние гипотезы ниже
|
|
||||||
оставлены для истории — они НЕ были причиной.
|
|
||||||
|
|
||||||
## КОРЕНЬ (подтверждён трассой + исходником)
|
|
||||||
|
|
||||||
`set_char_collision` (seg006:0723): `char_x_right = obj_x/2 + 58`, где
|
|
||||||
`load_frame_to_obj` (seg008:1728) считает `obj_x = 2*char_dx_forward(dx) - 116`
|
|
||||||
и **добавляет +1** для кадров «чётного пикселя»:
|
|
||||||
`if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x;`
|
|
||||||
(бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит).
|
|
||||||
Деление `obj_x/2` — C-усечение К НУЛЮ, поэтому при `e = x+dx <= 57`
|
|
||||||
(obj_x < 0, зона левого шва) поправка +1 даёт `char_x_right = e+1`, а при
|
|
||||||
e >= 58 формула сокращается к чистому `e`.
|
|
||||||
|
|
||||||
Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 —
|
|
||||||
бит 0x80 не стоит), имеет **char_x_right = 58** и порога leave-left (<=57)
|
|
||||||
НЕ достигает. Наш движок считал передний край как `Kid.x + dx` без поправки
|
|
||||||
→ 57 → ложный leave → пинг-понг.
|
|
||||||
|
|
||||||
Эталонный цикл SDLPoP (сн normalized по трассе): стойка x=61 → тап вправо →
|
|
||||||
safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61 →
|
|
||||||
seq47 dx(-4) → **x=57** (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58, у
|
|
||||||
всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ срабатывает;
|
|
||||||
тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61 (2/3 видно); тап →
|
|
||||||
bump → 57 … по кругу. Char.room и drawn_room НЕ меняются вообще.
|
|
||||||
|
|
||||||
## Фикс (pop_map.c)
|
|
||||||
|
|
||||||
`char_x_forward_edge()`: `e = char_dx_forward(dx); if (((flags ^ (dir<0 ?
|
|
||||||
0x80 : 0)) & 0x80) == 0 && e <= 57) e++;` — используется в `char_front_coll`
|
|
||||||
(коллизия/bump/edge_distance) и в `check_leave` (порог ухода). Плюс порт
|
|
||||||
doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого
|
|
||||||
выхода нет). `pop_leave_timer` (exit_room_timer) оставлен — он реален в
|
|
||||||
seg002/seg003.
|
|
||||||
|
|
||||||
## Гипотезы, ОТПАВШИЕ после трассы (история диагноза)
|
|
||||||
|
|
||||||
## Симптом (подтверждён пользователем + инструментально)
|
|
||||||
|
|
||||||
Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке
|
|
||||||
room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя
|
|
||||||
состояниями:
|
|
||||||
- **A**: показывается room6, Kid у левой кромки за решёткой (спрайт виден на 2/3);
|
|
||||||
- **B**: показывается room8, Kid у его правой кромки.
|
|
||||||
|
|
||||||
Эталон SDLPoP (описан пользователем): drawn_room **всегда остаётся room6**,
|
|
||||||
Kid осциллирует у кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ
|
|
||||||
переключается. То есть у нас лишний свитч A→B.
|
|
||||||
|
|
||||||
## Тестовый контекст
|
|
||||||
|
|
||||||
START_ROOM=6. Ворота room8(0,9) закрыты, видны в col0 room6. Kid-структура
|
|
||||||
kframe: см. pop_kid.h (frame,x,y,direction,curr_col,curr_row,action,...).
|
|
||||||
Адрес _Kid и статиков — в .sprinter-cc-roomtest/roomtest.map (сдвигается от
|
|
||||||
сборки к сборке). MAME: memory `mame_mcp_bridge`/`mame_hdd_test_disk`.
|
|
||||||
Отладочные: J/L — короткий шаг влево/вправо (эмуляция Shift+стрелка).
|
|
||||||
|
|
||||||
## Инструментальный диагноз (watchpoint на pop_leave_dir)
|
|
||||||
|
|
||||||
В момент лишнего свитча A→B: `pop_leave_dir=1 (LEFT)`, Kid **frame=15 (СТОЯ,
|
|
||||||
не transient!), Kid.x=57** (до репроекции +140). То есть:
|
|
||||||
- `char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57`.
|
|
||||||
- Порог leave-left (взгляд вправо): `char_x_right <= 57` → срабатывает РОВНО на 57.
|
|
||||||
- Грань ворот (где их держит коллизия) = **61** (`wall_dist_from_left[1]=10 +
|
|
||||||
coll_tile_left_xpos=51`). Между 57 и 61 — **зазор 4px**: Kid НЕ удержан
|
|
||||||
воротами (d=61−57=4≥0 → check_bumped не бампит), но уже на пороге ухода.
|
|
||||||
- Kid оседает на 57 из-за recoil отскока: seq_47 = `act(bumped), dx(-4),
|
|
||||||
frame_50, 51, 52`; SEQ_DX(−4) двигает Char.x на −4 суммарно; frame_50.dx=4
|
|
||||||
компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку
|
|
||||||
(frame15, dx=0) `char_x_right = Char.x = aligned−4 = 57`.
|
|
||||||
|
|
||||||
## Что ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)
|
|
||||||
|
|
||||||
- Формула char_x: `char_x_right = obj_x/2+58 = Char.x+frame.dx` (seg006
|
|
||||||
set_char_collision) — совпадает с нашим char_dx_forward.
|
|
||||||
- Позиция грани ворот: `get_left_wall_xpos = wall_dist_from_left[1](10) +
|
|
||||||
xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184−140)+7 = 61` — совпадает с нашим
|
|
||||||
(`x_bump[−1+5]=44`, +7, +10 = 61).
|
|
||||||
- Порог leave: SDLPoP leave_room looking-right `char_x_right<=57` — совпадает.
|
|
||||||
- Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15
|
|
||||||
(image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
|
|
||||||
- seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.
|
|
||||||
|
|
||||||
## Почему точечные фиксы НЕ работают
|
|
||||||
|
|
||||||
- Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА,
|
|
||||||
не SDLPoP (в leave_room такого нет); откачено.
|
|
||||||
- `exit_room_timer=2` (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как
|
|
||||||
pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасает:
|
|
||||||
положение Char.x=57 **устойчивое** (Kid стоит), а не transient — после
|
|
||||||
истечения таймера leave срабатывает.
|
|
||||||
- По коду SDLPoP raw-рассуждением раз за разом выходит, что leave-left ДОЛЖЕН
|
|
||||||
сработать и там (Char.x=57), но эталон показывает, что нет. Значит различие
|
|
||||||
не в этих кусках, а в ДИНАМИКЕ: где именно оседает Char.x у шва И как
|
|
||||||
вычисляется drawn_room.
|
|
||||||
|
|
||||||
## КОРЕНЬ (гипотеза)
|
|
||||||
|
|
||||||
Наш движок держит `drawn_room == комната_Kid` ВСЕГДА, а шов подделан:
|
|
||||||
- коллизия — самодельные снапшоты кромок соседа (g_lcol/g_rcol, 1 колонка) +
|
|
||||||
`x_bump[tcol=-1]`, а НЕ порт `curr_row_coll_room[]`/`get_all_coll_row`/
|
|
||||||
`xpos_in_drawn_room` из seg004;
|
|
||||||
- уход — мгновенный `enter_room(сосед)` при пороге char_x, а НЕ отложенный
|
|
||||||
drawn_room через `next_room`/`check_the_end`.
|
|
||||||
|
|
||||||
Из-за упрощённой коллизии Char.x у шва оседает иначе, чем в SDLPoP (у нас ровно
|
|
||||||
57 = порог), и/или drawn_room переключается там, где в оригинале нет.
|
|
||||||
|
|
||||||
## План S2/S3 (порт coll_room + отложенный drawn_room) — НЕ потребовался для
|
|
||||||
## этого бага (трасса показала: в эталоне Char.room/drawn_room вообще не
|
|
||||||
## меняются). Остаётся как задел под стражей/двух персонажей в кадре.
|
|
||||||
|
|
||||||
Правило проекта: SDLPoP — источник истины, переводить 1:1, НЕ рассуждать «как
|
|
||||||
должно быть». Нужно портировать связку целиком:
|
|
||||||
|
|
||||||
1. **Раздельные комнаты.** `Char.room` (реальная комната персонажа) ≠
|
|
||||||
`drawn_room` (отрисованная). У нас уже есть `kid_room` (S1) + рендер-смещение
|
|
||||||
`pop_kid_set_render_dx(∓140)` (порт xpos_in_drawn_room для СПРАЙТА).
|
|
||||||
|
|
||||||
2. **Коллизия по Char.room через coll_room (seg004, S2).** Портировать
|
|
||||||
`check_collisions` → `get_row_collision_data` (строки curr/below/above) →
|
|
||||||
`get_left_wall_xpos`/`get_right_wall_xpos` (по `xpos_in_drawn_room(x_bump[
|
|
||||||
col+FIRST_ONSCREEN_COLUMN])`, ±140 когда колонка принадлежит соседу) →
|
|
||||||
массивы `curr_row_coll_room[]`/`_flags[]`, `bump_col_left_of_wall`/
|
|
||||||
`bump_col_right_of_wall` → `check_bumped_look_left/right` → `bumped()`.
|
|
||||||
Тайлы берутся из РЕАЛЬНОЙ комнаты колонки (get_tile(room,col,row) для любой
|
|
||||||
комнаты), НЕ из снапшота. Именно это определяет, где Char.x оседает у шва.
|
|
||||||
|
|
||||||
3. **Отрисовка по drawn_room, персонаж со сдвигом.** Рисуем drawn_room; Kid
|
|
||||||
(и будущие стражи) — со смещением ±140, если их комната ≠ drawn_room.
|
|
||||||
|
|
||||||
4. **Отложенная смена drawn_room (seg002/seg000, S3).** Уход =
|
|
||||||
`leave_room` (seg002:0504, ПОРТ ЦЕЛИКОМ, включая up/down/side, guard'ы
|
|
||||||
climb/standup/turn, doortop-check для right) → `goto_other_room`
|
|
||||||
(seg002:0486: Char.room=сосед, x±140/y±189). `exit_room` (seg002:03C7):
|
|
||||||
`exit_room_timer` + `next_room = Char.room`. `check_the_end` (seg000:0FBD):
|
|
||||||
`if (next_room != 0 && next_room != drawn_room) drawn_room = next_room;
|
|
||||||
redraw`. drawn_room меняется ТОЛЬКО так, а не по нашему прямому enter_room.
|
|
||||||
|
|
||||||
Ключевой момент, который надо переводить БУКВАЛЬНО (а не выводить): порядок
|
|
||||||
вызовов в play_frame (seg000:863 `play_frame`: … play_kid_frame → exit_room →
|
|
||||||
check_the_end) + set_char_collision перед leave_room в exit_room.
|
|
||||||
|
|
||||||
Карта сегментов: seg002 (leave/goto/exit_room), seg004 (коллизия/coll_room/
|
|
||||||
bumped), seg006 (set_char_collision/play_seq/frame_table_kid), seg000
|
|
||||||
(play_frame/check_the_end/draw). Наши файлы: pop_map.c (коллизия/физика/leave),
|
|
||||||
roomtest.c (enter_room/drawn_room/главный цикл), pop_kid.c (кадры/рендер-сдвиг).
|
|
||||||
|
|
||||||
Реализовано на 2026-07-22: S1 (kid_room + render_dx) + pop_leave_timer.
|
|
||||||
S2/S3 отложены — баг закрыт odd-pixel поправкой (см. КОРЕНЬ выше).
|
|
||||||
Память: [[pop_seam_room_model]], [[sdlpop_odd_pixel_char_x]].
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# TODO: оптимизация отрисовки (записано 2026-07-29)
|
|
||||||
|
|
||||||
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
|
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
|
||||||
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
|
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
|
||||||
@@ -365,6 +283,7 @@ S2/S3 отложены — баг закрыт odd-pixel поправкой (с
|
|||||||
ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем
|
ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем
|
||||||
безусловно» корректен, но дорог.
|
безусловно» корректен, но дорог.
|
||||||
|
|
||||||
|
<a id="t-1"></a>
|
||||||
## T-1. Пики: перерисовывать по причине, а не безусловно
|
## T-1. Пики: перерисовывать по причине, а не безусловно
|
||||||
|
|
||||||
**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
|
**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
|
||||||
@@ -388,6 +307,9 @@ S2/S3 отложены — баг закрыт odd-pixel поправкой (с
|
|||||||
сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений
|
сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений
|
||||||
прямоугольников (см. память `manual_hints_over_auto_detect`).
|
прямоугольников (см. память `manual_hints_over_auto_detect`).
|
||||||
|
|
||||||
|
**Приоритет:** отдаётся почти бесплатно ПОСЛЕ T-2, отдельно не окупается.
|
||||||
|
|
||||||
|
<a id="t-2"></a>
|
||||||
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит
|
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит
|
||||||
|
|
||||||
**Сейчас:** `kid_heal` → `kid_draw` → `pop_fore_over_kid` идут каждый кадр,
|
**Сейчас:** `kid_heal` → `kid_draw` → `pop_fore_over_kid` идут каждый кадр,
|
||||||
@@ -402,9 +324,11 @@ S2/S3 отложены — баг закрыт odd-pixel поправкой (с
|
|||||||
неизменного кадра — иначе одна из двух страниц останется со старым
|
неизменного кадра — иначе одна из двух страниц останется со старым
|
||||||
содержимым. Условие «обе страницы уже получили это состояние».
|
содержимым. Условие «обе страницы уже получили это состояние».
|
||||||
|
|
||||||
**Связь с T-1:** после T-1 idle-skip не требует ничего дополнительного для
|
**Связь с T-1:** после T-2 пики отпадают сами — раз Кида не перерисовываем,
|
||||||
пик: раз Кида не перерисовываем — heal'а нет — стирать пики нечем — редрой
|
heal'а нет, стирать пики нечем, редрой не нужен.
|
||||||
не нужен, он отпадает сам.
|
|
||||||
|
**Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно
|
||||||
|
там, где тапают Shift+стрелку (см. остаток KBD-1 в [`TASKS.md`](TASKS.md)).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -416,24 +340,12 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
|
|||||||
исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа
|
исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа
|
||||||
единицы (`||` `||||` = 24). Убирается закомментированием одной строки.
|
единицы (`||` `||||` = 24). Убирается закомментированием одной строки.
|
||||||
|
|
||||||
|
Комнаты **13, 18, 24 недостижимы** в обычной игре — это свойство данных
|
||||||
|
уровня, разбор в [`bug_closed.md`](bug_closed.md) («НЕ БАГИ»). Приоритет
|
||||||
|
багов в них низкий.
|
||||||
|
|
||||||
**Связность уровня 1 (проверено по `res2001.bin`, links @1952).** Обход графа
|
Таблица заполнена на 5 строк из 24 — доделать в рамках L1-TRIAGE
|
||||||
от стартовой комнаты 1 показывает: комнаты **13, 18, 24 НЕДОСТИЖИМЫ** в
|
([`TASKS.md`](TASKS.md)).
|
||||||
обычной игре. У всех трёх ссылки наружу есть, а на них не ссылается никто
|
|
||||||
(24: `L→9`, но у 9 `R=0`; 13 и 18 связаны только друг с другом). Признак
|
|
||||||
«комнату выкинули из компоновки, связи не почистили» — несимметричные ссылки
|
|
||||||
ровно у этих трёх, у остальных 21 симметрия полная:
|
|
||||||
|
|
||||||
```
|
|
||||||
13 L→22, у 22 R=16 | 18 L→15, у 15 R=12 | 24 L→9, у 9 R=0
|
|
||||||
13 R→16, у 16 L=22 | 18 R→12, у 12 L=15
|
|
||||||
| 18 D→19, у 19 U=12
|
|
||||||
```
|
|
||||||
|
|
||||||
Следствие для обхода: в 13/18/24 возможен «мусор в шве» — наш рендер кромки
|
|
||||||
читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
|
|
||||||
считает. Это свойство данных, а не наш баг. Приоритет багов в этих трёх
|
|
||||||
комнатах — низкий (в игре их не видно).
|
|
||||||
|
|
||||||
| комната | статус | что не так |
|
| комната | статус | что не так |
|
||||||
|---------|--------|------------|
|
|---------|--------|------------|
|
||||||
@@ -448,7 +360,7 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
|
|||||||
| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) |
|
| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) |
|
||||||
| 10 | | |
|
| 10 | | |
|
||||||
| 11 | | |
|
| 11 | | |
|
||||||
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» |
|
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» в `bug_closed.md` |
|
||||||
| 13 | недостижима в игре | |
|
| 13 | недостижима в игре | |
|
||||||
| 14 | | |
|
| 14 | | |
|
||||||
| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y−3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
|
| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y−3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
|
||||||
@@ -461,3 +373,41 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
|
|||||||
| 22 | | |
|
| 22 | | |
|
||||||
| 23 | | |
|
| 23 | | |
|
||||||
| 24 | недостижима в игре | |
|
| 24 | недостижима в игре | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Заметки (отладка)
|
||||||
|
|
||||||
|
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
|
||||||
|
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
|
||||||
|
сторону = разворот (как в оригинале safe_step), движение со второго.
|
||||||
|
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
|
||||||
|
**S** — выдать меч.
|
||||||
|
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
|
||||||
|
Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
|
||||||
|
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
|
||||||
|
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в
|
||||||
|
настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
|
||||||
|
## Сырые наблюдения (прогон 2026-08-03) → корень
|
||||||
|
|
||||||
|
Формулировки с прогона, как записаны. Закрытые корни — в
|
||||||
|
[`bug_closed.md`](bug_closed.md).
|
||||||
|
|
||||||
|
| # | наблюдение (кратко) | корень | статус |
|
||||||
|
|---|---------------------|--------|--------|
|
||||||
|
| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт |
|
||||||
|
| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт |
|
||||||
|
| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт |
|
||||||
|
| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт |
|
||||||
|
| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт |
|
||||||
|
| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт |
|
||||||
|
| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) |
|
||||||
|
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) |
|
||||||
|
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт |
|
||||||
|
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт |
|
||||||
|
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт |
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
Активные баги на уровне 1 -
|
||||||
|
|
||||||
|
- После того как кувшин со здоровьем выпит и Кид попадает в комнату во второй раз
|
||||||
|
то иногда кувшин мы не показываем (это правильно) но показываем анимацию пузырьков над
|
||||||
|
кувшином (чего показывать больше не должны - нет кувшина, нет пузырьков),
|
||||||
|
а в комнате 14 пузырек вообще восстанавливается полностью после того как Кид
|
||||||
|
выйдет из комнаты и вернется в нее обратно (есть ощущение что даже в одной комнате
|
||||||
|
поведение пузырька - будет ли он восстановлен или нет зависит от чего-то непонятного.
|
||||||
|
(есть еще ощущение что первый выпитый кувшин не восстанавливается а остальные
|
||||||
|
восстанавливаются)
|
||||||
|
- Меч - один раз подобранный он больше не должен появляться в своей (15-ой) комнате
|
||||||
|
иногда он появляется снова, иногда он больше не появляется но на дальней стене начинают
|
||||||
|
моргать контуры кирпичной кладки (странно что она не впечатана в heal уровень потому
|
||||||
|
что она должна быть отрисована всегда). причем мерцает по странному - не как double-buff
|
||||||
|
а как буд-то вместо искры на мече - изредка на несколько (один логический?) кадров.
|
||||||
|
- комната 12 - опять проблема с телепортацией через решетку - решетка в 0,9 - Кид стоит
|
||||||
|
за шаг (пол-шага) от решетки - бежит и пробегает через закрытую решетку.
|
||||||
|
- после respawn - остаются разбитыми плиты что были разбиты, выпитыми пузырьки и прочее -
|
||||||
|
это надо сравнить с оригиналом (SDLPoP) - но главное другое - если до respawn какая-то
|
||||||
|
плита не была разбита то теперь если ее разбить, выйти из комнаты и вернуться - то
|
||||||
|
плита восстановится (еще интереснее вариант когда плита-потолок в комнате 12 разбивается,
|
||||||
|
Кид через проем поднимается в верхнюю комнату и поднявшись плита в новой комнате опять
|
||||||
|
закрывает проем.
|
||||||
|
- проблема с плитой из комнаты 12 в верхнюю и пузырек в комнате 22 - они возникают даже
|
||||||
|
без respawn - когда Кид заходит в комнату 22 из комнаты 16, выпивает пузырек, выходит опять
|
||||||
|
в комнату 16 и опять возвращается в комнату 22 - пузырек возникает снова. Когда Кид разбивает
|
||||||
|
плиту из комнаты 12 в комнату 16 то после подъема в комнату 16 плита опять возникает.
|
||||||
|
аналогично комната 17 - если войти в нее из комнаты 23, разбить плиты 1,5 и 1,6, опуститься на
|
||||||
|
2,3 и выпить пузырек. вернуться в комнату 23 и опять в 17 - то и плиты вернутся и пузырек.
|
||||||
|
но могут плита и пузырек не вернуться - такое ощущение что если это первая разбитая плита
|
||||||
|
(Кида мы перенесли в комнату 12 через cheats) - то разбитая плита остается разбитой.
|
||||||
|
- комната 6 - Кид бежит по уровню ноль справа налево (начало бега 0,9), делает длинный
|
||||||
|
прыжок между 0,6 и 0,7 - он пролетает во внутрь стены и респавнится - скриншоты в bugscreens
|
||||||
|
- иногда залипают все-таки клавиши (пока замечено на кнопке вверх - отлипает только после
|
||||||
|
следующего нажатия). надо проверить - есть ли возможность не только отслеживать события
|
||||||
|
нажатие/отжатие но и текущий статус нажатых кнопок. Может вообще отказаться от прямой работы
|
||||||
|
с клавиатурой а работать с нею через BIOS ?
|
||||||
|
- иногда таки залипает и shift =((((
|
||||||
|
- комната 5 - кнопка на 0,4 не открывает решетку/ворота на 0,5 - это двойная кнопка - она
|
||||||
|
должна сразу открывать ворота на 0,5 и на 0,9 (возможно у нас она работает только на 0,9)
|
||||||
|
возможно что логически решетка на 0,5 открывается но анимации ее открытия нету =(
|
||||||
|
- комната 5 - Кид падает с кнопки 0,6 на 2,7 - получает урон, присяд в 2,8 - когда встает
|
||||||
|
он проваливается еще ниже в комнату 6 на между 2,7-2,8.
|
||||||
|
- если стражник убивает Кида то Кид респавнится на месте смерти и стражник тут же его убивает
|
||||||
|
еще раз и так по циклу
|
||||||
|
|
||||||
|
--------
|
||||||
|
|
||||||
|
- проверить как отрисовывается Кид вместе с мертвым стражником - в оригинале стражник поверх
|
||||||
|
Кида (Кид проходит за телом стража), у нас Кид поверх (перед) стражем;
|
||||||
|
- сейчас у нас шифт не залипает но теперь он теряет статус нажатости - то есть при нажатом шифте
|
||||||
|
второе-треть-четвертое нажатие стрелки отрабатывает не как осторожный шаг а переводит Кида на бег
|
||||||
|
по идее с новым алгоритмом должно было бы корректно отрабатывать нажатие шифта ? теперь же залипаний
|
||||||
|
нету но есть фантомный отжим ? такое поведение работы с клавиатурой хуже чем залипание шифта (потому
|
||||||
|
что игрок надеется что шифт нажат и будет короткий шаг а Кид начинает бежать и может провалиться в
|
||||||
|
ловушку/яму или набежать на пики и погибнуть). то есть надо как-то более точно проверять статус нажатости
|
||||||
|
шифта.
|
||||||
|
то есть - когда нажат шифт и нажимаются стрелки то вариант редких пропусков стрелок предпочтительнее чем
|
||||||
|
вариант когда мы перестаем понимать что шифт нажат.
|
||||||
|
- после respawn - должны ли оживать стражники ? сейчас они остаются мертвые.
|
||||||
|
- небольшой баг отображения (такой баг у нас уже был и ты его фиксил) - комната 12 - две падающие плиты
|
||||||
|
рядом (0,1 и 0,2) - после падения осколки только от одной на 1,2 а от 0,1 на 1,1 осколков нету.
|
||||||
|
при том что в комнате 7 две плиты 0,5 и 0,6 падая на 2,5 и 2,6 дают двое осколков.
|
||||||
|
возможная причина - когда Кид пробегает в комнате 12 по падающим плитам то он убегает сразу в комнату 15
|
||||||
|
и плита 0,2 успевает упасть и поменять статус пола 1,2 на пол с осколками а плита 0,1 еще остается в падении
|
||||||
|
когда Кид уже уходит в комнату 15 и потому статус пола на 1,1 на 'пол с осколками' измениться не успевает
|
||||||
|
|
||||||
|
--------
|
||||||
|
|
||||||
|
Подумай вот над чем -
|
||||||
|
интересует возможность покрытия уже написаного кода автотестами (тесткейсами).
|
||||||
|
автотесты внутри MAME я думаю будут достаточно затруднительными, но поскольку это у нас plain-C
|
||||||
|
то можно наверное делать автотесты и на host OS.
|
||||||
|
возможно это нам поможет не создавать ситуации когда нам каждый раз приходится фиксить баги которые
|
||||||
|
мы уже фиксили (по крайней мере постараемся избегать регресса при реализации новых фич)
|
||||||
|
я понимаю что это скорее всего будет достаточно сложно совместить с тем что финальный продукт идет
|
||||||
|
для Sprinter. но подумать над этим надо.
|
||||||
|
|
||||||
|
Пока ничего не предпринимай, только подумай что и как можно сделать.
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 426 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 825 KiB |
|
After Width: | Height: | Size: 825 KiB |
|
After Width: | Height: | Size: 825 KiB |
@@ -0,0 +1,502 @@
|
|||||||
|
/*
|
||||||
|
* guards.c — ЛОГИКА стража, собирается в БАНК (--bank 1=guards.c, окно W3).
|
||||||
|
*
|
||||||
|
* Правила банка (проверено пробником tests/w3bankgfx):
|
||||||
|
* - состояние держим в W1/W2 (pop_guard.c): статики банка линкуются в
|
||||||
|
* страницу банка и снаружи не читаются;
|
||||||
|
* - звать можно W1/W2-код (pop_map, pop_kid, pop_geom, libc, примитивы
|
||||||
|
* libbgi) — скобка _bgi_begin/_bgi_end возвращает страницу БАНКА;
|
||||||
|
* - звать НЕЛЬЗЯ резидент W3 (pop_bg) — ни напрямую, ни транзитивно:
|
||||||
|
* пока исполняется банк, резидентной страницы в адресном пространстве
|
||||||
|
* нет. Поэтому фон/окклюзию за стража рисует главный цикл.
|
||||||
|
*
|
||||||
|
* Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000
|
||||||
|
* play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой:
|
||||||
|
* ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а
|
||||||
|
* дальше работает общий pop_control().
|
||||||
|
*/
|
||||||
|
#include "pop_guard.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_ctrl.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
#include "pop_geom.h"
|
||||||
|
#include "pop_trob.h"
|
||||||
|
#include "pop_state.h" /* pop_immortal — чит бессмертия */
|
||||||
|
|
||||||
|
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
|
||||||
|
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
|
||||||
|
|
||||||
|
#define TILE_GATE 4
|
||||||
|
#define TILE_DOORTOP_FLOOR 7
|
||||||
|
#define TILE_LOOSE 11
|
||||||
|
#define TILE_DOORTOP 12
|
||||||
|
#define TILE_CHOMPER 18
|
||||||
|
#define TILE_WALL 20
|
||||||
|
|
||||||
|
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
|
||||||
|
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его
|
||||||
|
* через границу банка дороже, чем повторить. */
|
||||||
|
static uint8_t tile_is_floor(uint8_t t)
|
||||||
|
{
|
||||||
|
switch (t) {
|
||||||
|
case 0: case 9: case 12: case 20:
|
||||||
|
case 26: case 27: case 28: case 29:
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid,
|
||||||
|
* seg003:761): xpos − 7 делится на ширину тайла с округлением вниз. */
|
||||||
|
static int8_t tile_col; /* колонка последнего tile_at_kid */
|
||||||
|
|
||||||
|
static uint8_t tile_at_kid(int16_t xpos)
|
||||||
|
{
|
||||||
|
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos − 7 − 58)/14),
|
||||||
|
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]).
|
||||||
|
* Оригинал берёт её из tile_div_tbl, у нас — делением с округлением
|
||||||
|
* вниз, как в pop_map. */
|
||||||
|
int16_t x = xpos - 7 - SCREENSPACE_X;
|
||||||
|
int16_t col = x / TILE_SIZEX;
|
||||||
|
if (x % TILE_SIZEX < 0) --col;
|
||||||
|
tile_col = (int8_t)col;
|
||||||
|
return pop_tile_at(tile_col, Kid.curr_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
|
||||||
|
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
|
||||||
|
* 2 видит и пойдёт. */
|
||||||
|
void pop_check_can_guard_see_kid(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t kid_frame = Kid.frame;
|
||||||
|
int16_t left_pos, right_pos;
|
||||||
|
uint8_t tile;
|
||||||
|
|
||||||
|
if (!(Guard.charid != 0 &&
|
||||||
|
/* кадры 219..228 — подъём по лестнице: там страж Кида не видит */
|
||||||
|
kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) &&
|
||||||
|
Kid.alive < 0 && Guard.alive < 0 &&
|
||||||
|
Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) {
|
||||||
|
can_guard_see_kid = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
can_guard_see_kid = 2;
|
||||||
|
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||||
|
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||||
|
if (left_pos > right_pos) {
|
||||||
|
int16_t t = left_pos; left_pos = right_pos; right_pos = t;
|
||||||
|
}
|
||||||
|
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
|
||||||
|
if (tile_at_kid(left_pos) == TILE_CHOMPER)
|
||||||
|
left_pos += TILE_SIZEX;
|
||||||
|
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
|
||||||
|
if (tile_at_kid(right_pos) == TILE_GATE)
|
||||||
|
right_pos -= TILE_SIZEX;
|
||||||
|
|
||||||
|
while (left_pos <= right_pos) {
|
||||||
|
tile = tile_at_kid(left_pos);
|
||||||
|
/* Сквозь это не видно вовсе. */
|
||||||
|
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
|
||||||
|
can_guard_see_kid = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
|
||||||
|
* упрётся в неоткрытые ворота / шагнёт в дыру. */
|
||||||
|
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
|
||||||
|
can_guard_see_kid = 1;
|
||||||
|
} else if (tile == TILE_GATE &&
|
||||||
|
pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) {
|
||||||
|
can_guard_see_kid = 1; /* ворота подняты не до конца */
|
||||||
|
}
|
||||||
|
left_pos += TILE_SIZEX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
|
||||||
|
static void move_0_nothing(void)
|
||||||
|
{
|
||||||
|
control_shift = control_y = control_x = control_shift2 = 0;
|
||||||
|
control_down = control_up = control_backward = control_forward = 0;
|
||||||
|
}
|
||||||
|
static void move_1_forward(void)
|
||||||
|
{
|
||||||
|
control_x = POP_CONTROL_HELD_FORWARD;
|
||||||
|
control_forward = POP_CONTROL_HELD;
|
||||||
|
}
|
||||||
|
static void move_2_backward(void)
|
||||||
|
{
|
||||||
|
control_backward = POP_CONTROL_HELD;
|
||||||
|
control_x = POP_CONTROL_HELD_BACKWARD;
|
||||||
|
}
|
||||||
|
static void move_3_up(void)
|
||||||
|
{
|
||||||
|
control_y = POP_CONTROL_HELD_UP;
|
||||||
|
control_up = POP_CONTROL_HELD;
|
||||||
|
}
|
||||||
|
static void move_4_down(void)
|
||||||
|
{
|
||||||
|
control_down = POP_CONTROL_HELD;
|
||||||
|
control_y = POP_CONTROL_HELD_DOWN;
|
||||||
|
}
|
||||||
|
static void move_6_shift(void)
|
||||||
|
{
|
||||||
|
control_shift = POP_CONTROL_HELD;
|
||||||
|
control_shift2 = POP_CONTROL_HELD;
|
||||||
|
}
|
||||||
|
static void move_down_back(void)
|
||||||
|
{
|
||||||
|
control_down = POP_CONTROL_HELD;
|
||||||
|
move_2_backward();
|
||||||
|
}
|
||||||
|
static void move_down_forw(void)
|
||||||
|
{
|
||||||
|
control_down = POP_CONTROL_HELD;
|
||||||
|
move_1_forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* autocontrol_guard_inactive (seg002:710): страж с убранным мечом.
|
||||||
|
* Пока Кид за спиной — страж не реагирует; исключение — «шум» Кида
|
||||||
|
* (is_guard_notice), от которого страж оборачивается. */
|
||||||
|
static void autocontrol_guard_inactive(void)
|
||||||
|
{
|
||||||
|
int16_t distance;
|
||||||
|
if (Kid.alive >= 0) return;
|
||||||
|
distance = pop_char_opp_dist();
|
||||||
|
/* В оригинале условие записано как (word)distance < (word)-8 —
|
||||||
|
* беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона −8..−1.
|
||||||
|
* То есть: Кид не «вплотную за спиной». */
|
||||||
|
if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) {
|
||||||
|
if (is_guard_notice) {
|
||||||
|
is_guard_notice = 0;
|
||||||
|
if (distance < 0) {
|
||||||
|
if (distance < -4) move_4_down(); /* обернуться на шум */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (distance < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (can_guard_see_kid) {
|
||||||
|
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
|
||||||
|
move_down_forw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Активный страж (меч обнажён), seg002:737.. -------------------- *
|
||||||
|
* Таблицы вероятностей по 12 градациям мастерства (seg002:26..38). Бросок
|
||||||
|
* — prandom(255) с СОБСТВЕННЫМ сидом: у нас последовательности разведены
|
||||||
|
* по доменам (кладка / анимация тайлов / бой), см. pop_geom.h. */
|
||||||
|
static const uint8_t STRIKEPROB[NUM_GUARD_SKILLS] = { 61,100, 61, 61, 61, 40,100,220, 0, 48, 32, 48};
|
||||||
|
static const uint8_t RESTRIKEPROB[NUM_GUARD_SKILLS] = { 0, 0, 0, 5, 5,175, 16, 8, 0,255,255,150};
|
||||||
|
static const uint8_t BLOCKPROB[NUM_GUARD_SKILLS] = { 0,150,150,200,200,255,200,250, 0,255,255,255};
|
||||||
|
static const uint8_t IMPBLOCKPROB[NUM_GUARD_SKILLS] = { 0, 61, 61,100,100,145,100,250, 0,145,255,175};
|
||||||
|
static const uint8_t ADVPROB[NUM_GUARD_SKILLS] = {255,200,200,200,255,255,200, 0, 0,255,100,100};
|
||||||
|
|
||||||
|
/* Бросок «сработало ли»: в оригинале prob > prandom(255). */
|
||||||
|
static uint8_t roll(uint8_t prob)
|
||||||
|
{
|
||||||
|
return (uint8_t)(prob > (uint8_t)pop_prandom(&pop_fight_seed, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* guard_advance (seg002:0AF5): наступать. Мастерство 0 наступает всегда,
|
||||||
|
* остальные — только пока Кид не замахнулся. */
|
||||||
|
static void guard_advance(void)
|
||||||
|
{
|
||||||
|
if (guard_skill == 0 || kid_sword_strike == 0) {
|
||||||
|
if (roll(ADVPROB[guard_skill])) move_1_forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* guard_block (seg002:0B1D): блокировать замах Кида. Если Кид только что
|
||||||
|
* пробил блок (justblocked) — своя, «импровизационная» вероятность. */
|
||||||
|
static void guard_block(void)
|
||||||
|
{
|
||||||
|
uint8_t of = Opp.frame;
|
||||||
|
if (of == FRAME_152_STRIKE_2 || of == FRAME_153_STRIKE_3 ||
|
||||||
|
of == FRAME_162_BLOCK_TO_STRIKE) {
|
||||||
|
if (roll(justblocked ? IMPBLOCKPROB[guard_skill]
|
||||||
|
: BLOCKPROB[guard_skill]))
|
||||||
|
move_3_up();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* guard_strike (seg002:0B73): бить. По замаху Кида не бьём (кадры 169/151);
|
||||||
|
* из своего парирования — по отдельной вероятности «добить». */
|
||||||
|
static void guard_strike(void)
|
||||||
|
{
|
||||||
|
uint8_t of = Opp.frame, cf = Char.frame;
|
||||||
|
if (of == FRAME_169_BEGIN_BLOCK || of == FRAME_151_STRIKE_1) return;
|
||||||
|
if (cf == FRAME_161_PARRY || cf == FRAME_150_PARRY) {
|
||||||
|
if (roll(RESTRIKEPROB[guard_skill])) move_6_shift();
|
||||||
|
} else {
|
||||||
|
if (roll(STRIKEPROB[guard_skill])) move_6_shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* autocontrol_guard_kid_armed (seg002:0AC1): Кид с мечом. Вплотную и
|
||||||
|
* далеко — наступаем; в «рабочей» дистанции 10..28 сначала пробуем блок, и
|
||||||
|
* только отдышавшись бьём. */
|
||||||
|
static void autocontrol_guard_kid_armed(int16_t distance)
|
||||||
|
{
|
||||||
|
if (distance < 10 || distance >= 29) {
|
||||||
|
guard_advance();
|
||||||
|
} else {
|
||||||
|
guard_block();
|
||||||
|
if (guard_refrac == 0) {
|
||||||
|
if (distance < 12 || distance >= 29) guard_advance();
|
||||||
|
else guard_strike();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* autocontrol_guard_kid_in_sight (seg002:0A93). */
|
||||||
|
static void autocontrol_guard_kid_in_sight(int16_t distance)
|
||||||
|
{
|
||||||
|
if (Opp.sword == SWORD_2_DRAWN) {
|
||||||
|
autocontrol_guard_kid_armed(distance);
|
||||||
|
} else if (guard_refrac == 0) {
|
||||||
|
/* Кид безоружен: подойти и рубить. */
|
||||||
|
if (distance < 29) move_6_shift();
|
||||||
|
else move_1_forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Тайл перед стражем / через один (порт get_tile_infrontof_char и
|
||||||
|
* get_tile_infrontof2_char, seg004) — но для Char, а не для Kid. */
|
||||||
|
static uint8_t tile_infront(int8_t step)
|
||||||
|
{
|
||||||
|
int8_t d = (int8_t)(Char.direction < 0 ? -step : step);
|
||||||
|
return pop_tile_at((int8_t)(Char.curr_col + d), Char.curr_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* autocontrol_guard_kid_far (seg002:09CB): идём, если впереди есть пол хотя
|
||||||
|
* бы через тайл; иначе пятимся (не шагать в пропасть). */
|
||||||
|
static void autocontrol_guard_kid_far(void)
|
||||||
|
{
|
||||||
|
if (tile_is_floor(tile_infront(1)) || tile_is_floor(tile_infront(2)))
|
||||||
|
move_1_forward();
|
||||||
|
else
|
||||||
|
move_2_backward();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* autocontrol_guard_active (seg002:737). */
|
||||||
|
static void autocontrol_guard_active(void)
|
||||||
|
{
|
||||||
|
uint8_t cf = Char.frame;
|
||||||
|
uint8_t of;
|
||||||
|
int16_t distance;
|
||||||
|
|
||||||
|
/* Кадр 166 (неактивная стойка) и всё, что ниже 150, — не боевые позы. */
|
||||||
|
if (cf == FRAME_166_STAND_INACTIVE || cf < 150 || can_guard_see_kid == 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (can_guard_see_kid == 0) {
|
||||||
|
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе
|
||||||
|
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */
|
||||||
|
if (Char.charid != CHARID_4_SKELETON) move_down_back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* can_guard_see_kid == 2 */
|
||||||
|
of = Opp.frame;
|
||||||
|
distance = pop_char_opp_dist();
|
||||||
|
/* Кид падает/приземляется в отскоке — не мешаем ему, ждём. */
|
||||||
|
if (distance >= 12 && of >= 102 && of < 118 && Opp.action == 5)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (distance < 35) {
|
||||||
|
if ((Char.sword < SWORD_2_DRAWN && distance < 8) || distance < 12) {
|
||||||
|
/* Слишком близко: если смотрим в одну сторону — развернуться. */
|
||||||
|
if (Char.direction == Opp.direction) move_2_backward();
|
||||||
|
else move_1_forward();
|
||||||
|
} else {
|
||||||
|
autocontrol_guard_kid_in_sight(distance);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guard_refrac != 0) return;
|
||||||
|
if (Char.direction != Opp.direction) {
|
||||||
|
/* Кид разбежался — встречаем ударом, а не шагом. */
|
||||||
|
if (of >= 7 && of < 15) { /* бег */
|
||||||
|
if (distance < 40) move_6_shift();
|
||||||
|
return;
|
||||||
|
} else if (of >= 34 && of < 44) { /* разбег в прыжок */
|
||||||
|
if (distance < 50) move_6_shift();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autocontrol_guard_kid_far();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
|
||||||
|
static void autocontrol_opponent(void)
|
||||||
|
{
|
||||||
|
move_0_nothing();
|
||||||
|
/* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */
|
||||||
|
if (justblocked) --justblocked;
|
||||||
|
if (kid_sword_strike) --kid_sword_strike;
|
||||||
|
if (guard_refrac) --guard_refrac;
|
||||||
|
if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
|
||||||
|
else autocontrol_guard_active();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */
|
||||||
|
#define ACTION_99_HURT 99
|
||||||
|
#define ACTION_1_RUN 1
|
||||||
|
#define FRAME_154_POKING 154
|
||||||
|
#define SEQ_69_ATTACK_WAS_PARRIED 69
|
||||||
|
#define SEQ_74_HIT_BY_SWORD 74
|
||||||
|
#define SEQ_85_STABBED_TO_DEATH 85
|
||||||
|
|
||||||
|
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
|
||||||
|
static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
|
||||||
|
{ 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0};
|
||||||
|
|
||||||
|
/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту.
|
||||||
|
* Возврат 1 — HP кончились. Чит бессмертия ловим здесь: анимацию удара
|
||||||
|
* Кид получает, а HP не теряет. */
|
||||||
|
static uint8_t take_hp(uint8_t count)
|
||||||
|
{
|
||||||
|
if (Char.charid == CHARID_0_KID) {
|
||||||
|
if (pop_immortal) return 0;
|
||||||
|
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
|
||||||
|
hitp_delta = (int8_t)-(int8_t)count;
|
||||||
|
} else {
|
||||||
|
if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
|
||||||
|
guardhp_delta = (int8_t)-(int8_t)count;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
|
||||||
|
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
|
||||||
|
* «получил удар».
|
||||||
|
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
|
||||||
|
* меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map
|
||||||
|
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
|
||||||
|
* идёт ровно нашей веткой. */
|
||||||
|
static void hurt_by_sword(void)
|
||||||
|
{
|
||||||
|
if (Char.alive >= 0) return;
|
||||||
|
/* Чит бессмертия перекрывает и «безоружного закалывают насмерть»:
|
||||||
|
* без этого Кид гибнет с одного удара мимо стойки, и чит бесполезен
|
||||||
|
* ровно там, где нужен. Анимацию удара оставляем. */
|
||||||
|
if (Char.charid == CHARID_0_KID && pop_immortal) {
|
||||||
|
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||||
|
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||||
|
Char.fall_y = 0;
|
||||||
|
play_seq();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Char.sword != SWORD_2_DRAWN) {
|
||||||
|
take_hp(100);
|
||||||
|
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
|
||||||
|
} else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) {
|
||||||
|
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
|
||||||
|
} else {
|
||||||
|
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||||
|
}
|
||||||
|
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||||
|
Char.fall_y = 0;
|
||||||
|
play_seq(); /* TODO: звук 12/13 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
|
||||||
|
* Бьют кадры 153/154; если соперник в этот момент парирует — удар отбит. */
|
||||||
|
static void check_hurting(void)
|
||||||
|
{
|
||||||
|
int16_t distance;
|
||||||
|
uint8_t cf, of, min_range;
|
||||||
|
|
||||||
|
if (Char.sword != SWORD_2_DRAWN) return;
|
||||||
|
if (Char.curr_row != Opp.curr_row) return;
|
||||||
|
cf = Char.frame;
|
||||||
|
if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return;
|
||||||
|
|
||||||
|
distance = pop_char_opp_dist();
|
||||||
|
of = Opp.frame;
|
||||||
|
if (distance < 0 || distance >= 29 ||
|
||||||
|
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
||||||
|
/* Соперник НЕ парирует. */
|
||||||
|
if (cf == FRAME_154_POKING) {
|
||||||
|
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
||||||
|
distance = pop_char_opp_dist();
|
||||||
|
if (distance >= (int16_t)min_range && distance < 29)
|
||||||
|
Opp.action = ACTION_99_HURT;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Парировано. */
|
||||||
|
Opp.frame = FRAME_161_PARRY;
|
||||||
|
if (Char.charid != CHARID_0_KID) justblocked = 4;
|
||||||
|
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
|
||||||
|
play_seq();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
|
||||||
|
void pop_check_sword_hurting(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t kf = Kid.frame;
|
||||||
|
if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */
|
||||||
|
pop_loadshad_and_opp();
|
||||||
|
check_hurting();
|
||||||
|
pop_saveshad_and_opp();
|
||||||
|
pop_loadkid_and_opp();
|
||||||
|
check_hurting();
|
||||||
|
pop_savekid_and_opp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если
|
||||||
|
* ранен страж — Киду в этом кадре урон уже не проходит (в оригинале так же:
|
||||||
|
* его action сбрасывается ДО проверки). */
|
||||||
|
void pop_check_sword_hurt(void) __banked
|
||||||
|
{
|
||||||
|
if (Guard.action == ACTION_99_HURT) {
|
||||||
|
if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN;
|
||||||
|
pop_loadshad();
|
||||||
|
hurt_by_sword();
|
||||||
|
pop_saveshad();
|
||||||
|
guard_refrac = (int8_t)REFRACTIMER[guard_skill];
|
||||||
|
} else if (Kid.action == ACTION_99_HURT) {
|
||||||
|
pop_loadkid();
|
||||||
|
hurt_by_sword();
|
||||||
|
pop_savekid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»:
|
||||||
|
* пока страж жив (alive < 0) и HP есть — обычный ход; как только HP дошли до
|
||||||
|
* нуля, он переводится в «умирает» (alive = 0), и дальше тот же control()
|
||||||
|
* ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто
|
||||||
|
* доигрывает последовательность. */
|
||||||
|
static void play_guard(void)
|
||||||
|
{
|
||||||
|
if (Char.alive < 0 && guardhp_curr == 0) {
|
||||||
|
Char.alive = 0;
|
||||||
|
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
|
||||||
|
* которого нет; для уровня 1 других эффектов там нет. */
|
||||||
|
}
|
||||||
|
autocontrol_opponent();
|
||||||
|
pop_control();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у
|
||||||
|
* нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора
|
||||||
|
* последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) —
|
||||||
|
* отдельным шагом. */
|
||||||
|
void pop_guard_tick(void) __banked
|
||||||
|
{
|
||||||
|
if (Guard.charid == 0) return; /* стража в комнате нет */
|
||||||
|
pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */
|
||||||
|
/* Симметрично Киду (play_guard_frame, seg000:1248): cur_frame один на
|
||||||
|
* всех, и до этой строки в нём лежит кадр КИДА — он тикал раньше. */
|
||||||
|
pop_load_fram_det_col();
|
||||||
|
play_guard();
|
||||||
|
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
|
||||||
|
* выбирается по Char.charid внутри load_frame (seg006:0293). */
|
||||||
|
play_seq();
|
||||||
|
pop_saveshad();
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -8,449 +8,20 @@
|
|||||||
|
|
||||||
#define SEQTBL_BASE 0x196Eu
|
#define SEQTBL_BASE 0x196Eu
|
||||||
|
|
||||||
/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */
|
/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid).
|
||||||
|
* САМА таблица кадров и seqtbl лежат в kid_data.bin (EMM-
|
||||||
|
* страница); здесь только тип и размеры. */
|
||||||
typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe;
|
typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe;
|
||||||
#define KID_NFRAMES 241
|
#define KID_NFRAMES 241
|
||||||
static const kframe kid_frames[KID_NFRAMES] = {
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{ 0, 1, 0,0xC4,0x00},
|
|
||||||
{ 1, 1, 0,0x44,0x00},
|
|
||||||
{ 2, 3, 0,0x47,0x00},
|
|
||||||
{ 3, 4, 0,0x48,0x00},
|
|
||||||
{ 4, 0, 0,0xE6,0x00},
|
|
||||||
{ 5, 0, 0,0x49,0x00},
|
|
||||||
{ 6, 0, 0,0x4A,0x00},
|
|
||||||
{ 7, 0, 0,0xC5,0x00},
|
|
||||||
{ 8, 0, 0,0x44,0x00},
|
|
||||||
{ 9, 0, 0,0x47,0x00},
|
|
||||||
{ 10, 0, 0,0x4B,0x00},
|
|
||||||
{ 11, 0, 0,0x43,0x00},
|
|
||||||
{ 12, 0, 0,0xC3,0x00},
|
|
||||||
{ 13, 0, 0,0x47,0x00},
|
|
||||||
{ 14, 0, 0,0x43,0x09},
|
|
||||||
{ 15, 0, 0,0xC3,0x00},
|
|
||||||
{ 16, 0, 0,0x44,0x00},
|
|
||||||
{ 17, 0, 0,0x46,0x00},
|
|
||||||
{ 18, 0, 0,0x48,0x00},
|
|
||||||
{ 19, 0, 0,0x89,0x00},
|
|
||||||
{ 20, 0, 0,0x0B,0x00},
|
|
||||||
{ 21, 0, 0,0x8B,0x00},
|
|
||||||
{ 22, 0, 0,0x11,0x00},
|
|
||||||
{ 23, 0, 0,0x07,0x00},
|
|
||||||
{ 24, 0, 0,0x05,0x00},
|
|
||||||
{ 25, 0, 0,0xC1,0x00},
|
|
||||||
{ 26, 0, 0,0xC6,0x00},
|
|
||||||
{ 27, 0, 0,0x43,0x00},
|
|
||||||
{ 28, 0, 0,0x48,0x00},
|
|
||||||
{ 29, 0, 0,0x42,0x00},
|
|
||||||
{ 30, 0, 0,0x42,0x00},
|
|
||||||
{ 31, 0, 0,0xC2,0x00},
|
|
||||||
{ 32, 0, 0,0xC2,0x00},
|
|
||||||
{ 33, 0, 0,0x43,0x00},
|
|
||||||
{ 34, 0, 0,0x48,0x00},
|
|
||||||
{ 35, 0, 0,0xCE,0x00},
|
|
||||||
{ 36, 0, 0,0xC1,0x00},
|
|
||||||
{ 37, 0, 0,0x45,0x00},
|
|
||||||
{ 38, 0, 0,0x8E,0x00},
|
|
||||||
{ 39, 0, 0,0x0B,0x00},
|
|
||||||
{ 40, 0, 0,0x8B,0x00},
|
|
||||||
{ 41, 0, 0,0x8A,0x00},
|
|
||||||
{ 42, 0, 0,0x01,0x00},
|
|
||||||
{ 43, 0, 0,0xC4,0x00},
|
|
||||||
{ 44, 0, 0,0xC3,0x00},
|
|
||||||
{ 45, 0, 0,0xC3,0x00},
|
|
||||||
{ 46, 0, 0,0xA5,0x00},
|
|
||||||
{ 47, 0, 0,0xA4,0x00},
|
|
||||||
{ 48, 0, 0,0x66,0x00},
|
|
||||||
{ 49, 4, 0,0x67,0x00},
|
|
||||||
{ 50, 3, 0,0x66,0x00},
|
|
||||||
{ 51, 1, 0,0x44,0x00},
|
|
||||||
{ 64, 0, 0,0xC2,0x00},
|
|
||||||
{ 65, 0, 0,0x41,0x00},
|
|
||||||
{ 66, 0, 0,0x42,0x00},
|
|
||||||
{ 67, 0, 0,0x00,0x00},
|
|
||||||
{ 68, 0, 0,0x00,0x00},
|
|
||||||
{ 69, 0, 0,0x80,0x00},
|
|
||||||
{ 70, 0, 0,0x00,0x00},
|
|
||||||
{ 71, 0, 0,0x80,0x00},
|
|
||||||
{ 72, 0, 0,0x00,0x00},
|
|
||||||
{ 73, 0, 0,0x80,0x00},
|
|
||||||
{ 74, 0, 0,0x00,0x00},
|
|
||||||
{ 75, 0, 0,0x00,0x00},
|
|
||||||
{ 76, 0, 0,0x80,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{ 80, -2, 0,0x41,0x00},
|
|
||||||
{ 81, -2, 0,0x41,0x00},
|
|
||||||
{ 82, -1, 0,0xC2,0x00},
|
|
||||||
{ 83, -2, 0,0x42,0x00},
|
|
||||||
{ 84, -2, 0,0x41,0x00},
|
|
||||||
{ 85, -2, 0,0x41,0x00},
|
|
||||||
{ 86, -2, 0,0x41,0x00},
|
|
||||||
{ 87, -1, 0,0x07,0x00},
|
|
||||||
{ 88, -1, 0,0x05,0x00},
|
|
||||||
{ 89, 2, 0,0x07,0x00},
|
|
||||||
{ 90, 2, 0,0x07,0x00},
|
|
||||||
{ 91, 2, -3,0x00,0x00},
|
|
||||||
{ 92, 2, -10,0x00,0x00},
|
|
||||||
{ 93, 2, -11,0x80,0x00},
|
|
||||||
{ 94, 3, -2,0x43,0x00},
|
|
||||||
{ 95, 3, 0,0xC3,0x00},
|
|
||||||
{ 96, 3, 0,0xC3,0x00},
|
|
||||||
{ 97, 3, 0,0x63,0x00},
|
|
||||||
{ 98, 4, 0,0xE3,0x00},
|
|
||||||
{ 28, 0, 0,0x00,0x00},
|
|
||||||
{ 99, 7, -14,0x80,0x00},
|
|
||||||
{100, 7, -12,0x80,0x00},
|
|
||||||
{101, 4, -12,0x00,0x00},
|
|
||||||
{102, 3, -10,0x80,0x00},
|
|
||||||
{103, 2, -10,0x80,0x00},
|
|
||||||
{104, 1, -10,0x80,0x00},
|
|
||||||
{105, 0, -11,0x00,0x00},
|
|
||||||
{106, -1, -12,0x00,0x00},
|
|
||||||
{107, -1, -14,0x00,0x00},
|
|
||||||
{108, -1, -14,0x00,0x00},
|
|
||||||
{109, -1, -15,0x80,0x00},
|
|
||||||
{110, -1, -15,0x80,0x00},
|
|
||||||
{111, 0, -15,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{112, 0, 0,0xC6,0x00},
|
|
||||||
{113, 0, 0,0x46,0x00},
|
|
||||||
{114, 0, 0,0xC5,0x00},
|
|
||||||
{115, 0, 0,0x45,0x00},
|
|
||||||
{116, 0, 0,0xC2,0x00},
|
|
||||||
{117, 0, 0,0xC4,0x00},
|
|
||||||
{118, 0, 0,0xC5,0x00},
|
|
||||||
{119, 0, 0,0x46,0x00},
|
|
||||||
{120, 0, 0,0x47,0x00},
|
|
||||||
{121, 0, 0,0x47,0x00},
|
|
||||||
{122, 0, 0,0x49,0x00},
|
|
||||||
{123, 0, 0,0xC8,0x00},
|
|
||||||
{124, 0, 0,0xC9,0x00},
|
|
||||||
{125, 0, 0,0x49,0x00},
|
|
||||||
{126, 0, 0,0x45,0x00},
|
|
||||||
{127, 2, 0,0x45,0x00},
|
|
||||||
{128, 2, 0,0xC5,0x00},
|
|
||||||
{129, 0, 0,0xC3,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{133, 0, 0,0x43,0x00},
|
|
||||||
{134, 0, 0,0xC4,0x00},
|
|
||||||
{135, 0, 0,0xC5,0x00},
|
|
||||||
{136, 0, 0,0x48,0x00},
|
|
||||||
{137, 0, 0,0x6C,0x00},
|
|
||||||
{138, 0, 0,0xEF,0x00},
|
|
||||||
{139, 0, 0,0x63,0x00},
|
|
||||||
{140, 0, 0,0xC3,0x00},
|
|
||||||
{141, 0, 0,0x43,0x00},
|
|
||||||
{142, 0, 0,0x43,0x00},
|
|
||||||
{143, 0, 0,0x44,0x00},
|
|
||||||
{144, 0, 0,0x44,0x00},
|
|
||||||
{172, 0, 1,0xC1,0x00},
|
|
||||||
{173, 0, 1,0xC7,0x00},
|
|
||||||
{145, 0, -12,0x01,0x00},
|
|
||||||
{146, 0, -21,0x00,0x00},
|
|
||||||
{147, 1, -26,0x80,0x00},
|
|
||||||
{148, 4, -32,0x80,0x00},
|
|
||||||
{149, 6, -36,0x81,0x00},
|
|
||||||
{150, 7, -41,0x82,0x00},
|
|
||||||
{151, 2, 17,0x42,0x00},
|
|
||||||
{152, 4, 9,0xC4,0x00},
|
|
||||||
{153, 4, 5,0xC9,0x00},
|
|
||||||
{154, 4, 4,0xC8,0x00},
|
|
||||||
{155, 5, 0,0x69,0x00},
|
|
||||||
{156, 5, 0,0xE9,0x00},
|
|
||||||
{157, 5, 0,0xE8,0x00},
|
|
||||||
{158, 5, 0,0x69,0x00},
|
|
||||||
{159, 5, 0,0x69,0x00},
|
|
||||||
{184, 0, 2,0x80,0x10},
|
|
||||||
{174, 0, 2,0x80,0x1A},
|
|
||||||
{175, 3, 2,0x00,0x12},
|
|
||||||
{176, 7, 2,0xC4,0x16},
|
|
||||||
{177, 10, 2,0x00,0x15},
|
|
||||||
{178, 7, 2,0x80,0x17},
|
|
||||||
{179, 4, 2,0x80,0x19},
|
|
||||||
{180, 0, 2,0xCE,0x18},
|
|
||||||
{181, 0, 2,0xCD,0x0F},
|
|
||||||
{182, 3, 2,0x00,0x14},
|
|
||||||
{183, 3, 2,0x00,0x1F},
|
|
||||||
{184, 0, 2,0x80,0x10},
|
|
||||||
{185, 0, 2,0x80,0x11},
|
|
||||||
{186, 0, 2,0x00,0x20},
|
|
||||||
{187, 0, 2,0x80,0x21},
|
|
||||||
{188, 2, 2,0xC3,0x22},
|
|
||||||
{ 14, 0, 0,0x43,0x00},
|
|
||||||
{189, 7, 2,0x80,0x13},
|
|
||||||
{190, 1, 2,0x80,0x0E},
|
|
||||||
{191, 0, 2,0x80,0x1B},
|
|
||||||
{181, 0, 2,0xCD,0x0F},
|
|
||||||
{181, 0, 2,0xCD,0x0F},
|
|
||||||
{112, 0, 0,0xC6,0x2B},
|
|
||||||
{113, 0, 0,0x46,0x2C},
|
|
||||||
{114, 0, 0,0xC5,0x2D},
|
|
||||||
{115, 0, 0,0x45,0x2E},
|
|
||||||
{114, 0, 0,0xC5,0x00},
|
|
||||||
{ 78, 0, 3,0x8A,0x00},
|
|
||||||
{ 77, 4, 3,0x87,0x00},
|
|
||||||
{211, 0, 1,0x44,0x00},
|
|
||||||
{212, 0, 1,0x44,0x00},
|
|
||||||
{213, 0, 1,0x44,0x00},
|
|
||||||
{214, 0, 1,0x47,0x00},
|
|
||||||
{215, 0, 7,0x4B,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{ 79, 4, 7,0x49,0x00},
|
|
||||||
{130, 0, 0,0x44,0x00},
|
|
||||||
{131, 0, 0,0x44,0x00},
|
|
||||||
{132, 0, 2,0x44,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{192, 0, 0,0x00,0x00},
|
|
||||||
{193, 0, 1,0x00,0x00},
|
|
||||||
{194, 0, 0,0x80,0x00},
|
|
||||||
{195, 0, 0,0x00,0x00},
|
|
||||||
{196, -1, 0,0x00,0x00},
|
|
||||||
{197, -1, 0,0x00,0x00},
|
|
||||||
{198, -1, 0,0x00,0x00},
|
|
||||||
{199, -4, 0,0x00,0x00},
|
|
||||||
{200, -4, 0,0x80,0x00},
|
|
||||||
{201, -4, 0,0x00,0x00},
|
|
||||||
{202, -4, 0,0x00,0x00},
|
|
||||||
{203, -4, 0,0x00,0x00},
|
|
||||||
{204, -4, 0,0x00,0x00},
|
|
||||||
{205, -5, 0,0x00,0x00},
|
|
||||||
{206, -5, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{207, 0, 1,0x46,0x00},
|
|
||||||
{208, 0, 1,0xC6,0x00},
|
|
||||||
{209, 0, 1,0xC8,0x00},
|
|
||||||
{210, 0, 1,0x4A,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{255, 0, 0,0x00,0x00},
|
|
||||||
{ 52, 0, 0,0x80,0x00},
|
|
||||||
{ 53, 0, 0,0x00,0x00},
|
|
||||||
{ 54, 0, 0,0x00,0x00},
|
|
||||||
{ 55, 0, 0,0x00,0x00},
|
|
||||||
{ 56, 0, 0,0x80,0x00},
|
|
||||||
{ 57, 0, 0,0x00,0x00},
|
|
||||||
{ 58, 0, 0,0x00,0x00},
|
|
||||||
{ 59, 0, 0,0x00,0x00},
|
|
||||||
{ 60, 0, 0,0x80,0x00},
|
|
||||||
{ 61, 0, 0,0x00,0x00},
|
|
||||||
{ 62, 0, 0,0x80,0x00},
|
|
||||||
{ 63, 0, 0,0x00,0x00},
|
|
||||||
{160, 1, 1,0xC3,0x23},
|
|
||||||
{161, 0, 1,0x49,0x24},
|
|
||||||
{162, 0, 1,0xC3,0x25},
|
|
||||||
{163, 0, 1,0x49,0x26},
|
|
||||||
{164, 0, 1,0xC3,0x27},
|
|
||||||
{165, 1, 1,0x49,0x28},
|
|
||||||
{166, 1, 1,0x43,0x29},
|
|
||||||
{167, 1, 1,0xC9,0x2A},
|
|
||||||
{168, 4, 1,0xC6,0x00},
|
|
||||||
{169, 3, 1,0xCA,0x00},
|
|
||||||
{170, 1, 1,0x43,0x00},
|
|
||||||
{171, 1, 1,0xC8,0x00},
|
|
||||||
};
|
|
||||||
|
|
||||||
#define KID_SEQTBL_LEN 2310
|
#define KID_SEQTBL_LEN 2310
|
||||||
static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {
|
|
||||||
0xF9,0x01,0xFF,0x81,0x19,0xF9,0x01,0x01,0x02,0x03,0x04,0xFB,
|
/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */
|
||||||
0x08,0x05,0xFB,0x03,0x06,0xFB,0x03,0x07,0xFB,0x05,0x08,0xFB,
|
#define KID_BIN_FRAMES_OFF 0
|
||||||
0x01,0xF2,0x01,0x09,0xFB,0x02,0x0A,0xFB,0x04,0x0B,0xFB,0x05,
|
#define KID_BIN_SEQTBL_OFF 1205
|
||||||
0x0C,0xFB,0x02,0xF2,0x01,0x0D,0xFB,0x03,0x0E,0xFB,0x04,0xFF,
|
/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс
|
||||||
0x81,0x19,0xF9,0x00,0x0F,0xFF,0xA0,0x19,0xF9,0x01,0xA6,0xFF,
|
* frame + add_frame - 149 (add_frame=70 для кадров 102..106). */
|
||||||
0xA8,0x19,0xF9,0x05,0xFB,0x0A,0xB1,0xB1,0xFB,0xF9,0xFA,0xFE,
|
#define KID_NGFRAMES 41
|
||||||
0xB2,0xFB,0x05,0xFA,0x02,0xA6,0xFB,0xFF,0xFF,0xD2,0x19,0xFF,
|
#define KID_BIN_GFRAMES_OFF 3515
|
||||||
0xD2,0x19,0xF9,0x01,0xFB,0x02,0xCF,0xD0,0xFB,0x02,0xD1,0xFB,
|
|
||||||
0x02,0xD2,0xFB,0x03,0xF9,0x01,0xF2,0x00,0x9E,0xAA,0xAB,0xFF,
|
|
||||||
0xD8,0x19,0xF9,0x05,0xF8,0xFF,0x00,0xAC,0xFB,0xFF,0xFA,0x01,
|
|
||||||
0xAD,0xFB,0xFF,0xAE,0xFB,0xFF,0xFA,0x02,0xFB,0xFE,0xFA,0x01,
|
|
||||||
0xFB,0xFB,0xFA,0xFC,0xFF,0x4D,0x1A,0xF9,0x01,0xF8,0x01,0x00,
|
|
||||||
0x9B,0xFB,0x02,0xA5,0xFB,0xFE,0xFF,0xD2,0x19,0xF9,0x01,0xF8,
|
|
||||||
0xFF,0x00,0x9B,0x9C,0x9D,0x9E,0xFF,0x2E,0x1A,0xF9,0x01,0xF8,
|
|
||||||
0x01,0x00,0xFB,0x02,0xA3,0xFB,0x04,0xA4,0xA5,0xFF,0xD2,0x19,
|
|
||||||
0xF9,0x01,0xF8,0x01,0x00,0xFB,0x06,0xA4,0xA5,0xFF,0xD2,0x19,
|
|
||||||
0xF9,0x01,0xF8,0xFF,0x00,0xFB,0xFD,0xA0,0xFB,0xFE,0x9D,0xFF,
|
|
||||||
0xD2,0x19,0xF9,0x01,0xF8,0xFF,0x00,0xA8,0xF9,0x01,0x97,0xF9,
|
|
||||||
0x01,0x98,0x99,0x9A,0xF9,0x05,0x9B,0xF9,0x01,0x9C,0x9D,0xFF,
|
|
||||||
0xD2,0x19,0xF9,0x01,0xA7,0xFF,0x4A,0x1A,0xA2,0xFF,0x45,0x1A,
|
|
||||||
0xA9,0x96,0xFF,0xD2,0x19,0x9F,0xA0,0xFF,0x5F,0x1A,0xF9,0x01,
|
|
||||||
0xF4,0xFF,0xD2,0x19,0xF9,0x05,0xFB,0xF8,0xFF,0xD2,0x19,0xF9,
|
|
||||||
0x05,0xA0,0x9D,0xFF,0xD2,0x19,0xF9,0x07,0xFB,0xF8,0xFF,0x39,
|
|
||||||
0x1B,0xF9,0x05,0xFE,0xFB,0x05,0xFF,0x2E,0x1A,0xF9,0x05,0xFE,
|
|
||||||
0xFB,0x12,0xFF,0xA6,0x19,0xF9,0x01,0x10,0x11,0xFB,0x02,0x12,
|
|
||||||
0xFB,0x02,0x13,0xFB,0x02,0x14,0xFB,0x02,0x15,0xFB,0x02,0x16,
|
|
||||||
0xFB,0x07,0x17,0xFB,0x09,0x18,0xFB,0x05,0xFA,0xFA,0x19,0xFB,
|
|
||||||
0x01,0xFA,0x06,0x1A,0xFB,0x04,0xF4,0xF2,0x01,0x1B,0xFB,0xFD,
|
|
||||||
0x1C,0xFB,0x05,0x1D,0xF2,0x01,0x1E,0x1F,0x20,0x21,0xFB,0x01,
|
|
||||||
0xFF,0xA0,0x19,0xF9,0x01,0xF2,0x01,0x22,0xFB,0x05,0x23,0xFB,
|
|
||||||
0x06,0x24,0xFB,0x03,0x25,0xFB,0x05,0xF2,0x01,0x26,0xFB,0x07,
|
|
||||||
0x27,0xFB,0x0C,0xFA,0xFD,0x28,0xFB,0x08,0xFA,0xF7,0x29,0xFB,
|
|
||||||
0x08,0xFA,0xFE,0x2A,0xFB,0x04,0xFA,0x0B,0x2B,0xFB,0x04,0xFA,
|
|
||||||
0x03,0x2C,0xFB,0x05,0xF4,0xF2,0x01,0xFF,0x81,0x19,0xF9,0x01,
|
|
||||||
0xFB,0x01,0x6B,0xFB,0x02,0xFB,0x02,0x6C,0xFB,0x02,0x6D,0xFB,
|
|
||||||
0x02,0x6D,0xFB,0x02,0x6D,0xFF,0x16,0x1B,0x00,0xF9,0x01,0xFB,
|
|
||||||
0x01,0x6E,0x6F,0xFB,0x02,0x70,0xFB,0x02,0x6C,0xFB,0x02,0x6D,
|
|
||||||
0xFF,0x29,0x1B,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
|
|
||||||
0xFF,0xC4,0x19,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
|
|
||||||
0xFB,0x02,0x2F,0xFB,0xFF,0x30,0xFB,0x01,0x31,0xFB,0xFE,0x32,
|
|
||||||
0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,0xFF,0xFF,0x75,0x19,
|
|
||||||
0xF9,0x01,0xFB,0x01,0x35,0xFB,0x01,0xF2,0x01,0x36,0xFB,0x08,
|
|
||||||
0x37,0xF2,0x01,0x38,0xFB,0x07,0x39,0xFB,0x03,0x3A,0xFB,0x01,
|
|
||||||
0x3B,0x3C,0xFB,0x02,0x3D,0xFB,0xFF,0x3E,0x3F,0x40,0xFB,0xFF,
|
|
||||||
0x41,0xFB,0xF2,0xFE,0xFF,0x95,0x19,0xF9,0x03,0xFA,0xFF,0x66,
|
|
||||||
0xFB,0xFE,0xFA,0x06,0x67,0xFB,0xFE,0xFA,0x09,0x68,0xFB,0xFF,
|
|
||||||
0xFA,0x0C,0x69,0xFB,0xFD,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0xF9,
|
|
||||||
0x03,0xFA,0xFF,0xFB,0xFE,0x66,0xFB,0xFD,0xFA,0x06,0x67,0xFB,
|
|
||||||
0xFD,0xFA,0x09,0x68,0xFB,0xFE,0xFA,0x0C,0x69,0xFB,0xFD,0xF8,
|
|
||||||
0x00,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0xFF,0x66,
|
|
||||||
0xFB,0x02,0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,
|
|
||||||
0x69,0xFB,0xFE,0xF8,0x01,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,
|
|
||||||
0x01,0xFA,0x03,0xF7,0x06,0x1C,0x66,0xFB,0x02,0xFA,0x06,0x67,
|
|
||||||
0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,0xFE,0xF8,0x01,
|
|
||||||
0x0F,0xFF,0x49,0x1D,0xFB,0xFF,0xFA,0xFD,0xFF,0xE4,0x1B,0xFB,
|
|
||||||
0x01,0xFF,0xDB,0x1B,0x66,0xFB,0x02,0xFA,0x03,0x67,0xFB,0xFF,
|
|
||||||
0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,0x01,0x06,0xFF,
|
|
||||||
0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,0x66,0xFB,0x02,0xFA,
|
|
||||||
0x06,0x67,0xFB,0x01,0xFA,0x09,0x68,0xFB,0x02,0xFA,0x0C,0x69,
|
|
||||||
0xF8,0x02,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,
|
|
||||||
0x66,0xFB,0x03,0xFA,0x06,0x67,0xFB,0x02,0xFA,0x09,0x68,0xFB,
|
|
||||||
0x03,0xFA,0x0C,0x69,0xF8,0x03,0x0F,0xFF,0x49,0x1D,0xF9,0x01,
|
|
||||||
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,
|
|
||||||
0x02,0x4E,0x4F,0x50,0xFF,0xA1,0x1C,0xF9,0x01,0x43,0x44,0x45,
|
|
||||||
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,0x02,0xFB,0x01,
|
|
||||||
0x4E,0xFB,0x02,0x4F,0xFB,0x01,0x50,0xFF,0xA1,0x1C,0xF9,0x01,
|
|
||||||
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFB,0xFF,
|
|
||||||
0x4D,0xF9,0x02,0xFB,0xFE,0x4E,0xFB,0xFF,0x4F,0xFB,0xFF,0x50,
|
|
||||||
0xFF,0xA1,0x1C,0xF9,0x02,0x5B,0x5A,0x59,0x58,0x57,0x57,0x57,
|
|
||||||
0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63,
|
|
||||||
0x61,0x60,0x5F,0x5E,0x5D,0x5C,0x5B,0x5A,0x59,0x58,0x57,0x58,
|
|
||||||
0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x5F,0x5E,0x5D,0x5C,
|
|
||||||
0xFF,0x25,0x1D,0xF9,0x06,0x5C,0x5D,0x5D,0x5C,0x5C,0x5B,0xFF,
|
|
||||||
0xD8,0x1C,0x87,0x88,0x89,0x89,0x8A,0x8A,0x8A,0x8A,0x89,0x88,
|
|
||||||
0x87,0xFB,0xF9,0xFF,0x25,0x1D,0xF9,0x01,0x94,0x91,0x90,0x8F,
|
|
||||||
0x8E,0x8D,0xFB,0xFB,0xFA,0x3F,0xFC,0xF9,0x03,0x8C,0x8A,0x88,
|
|
||||||
0x5B,0xF9,0x02,0xFF,0xA4,0x1C,0xF9,0x01,0x87,0x88,0x89,0x8A,
|
|
||||||
0x8B,0x8C,0xFB,0x05,0xFA,0xC1,0xFD,0x8D,0x8E,0x8F,0x90,0x91,
|
|
||||||
0x92,0x93,0x94,0xF9,0x05,0x95,0xF9,0x01,0x76,0x77,0xFB,0x01,
|
|
||||||
0xFF,0xA0,0x19,0x51,0x52,0xF9,0x05,0x53,0xF9,0x01,0xF4,0xF2,
|
|
||||||
0x00,0x54,0x55,0xFB,0x03,0xFF,0xA0,0x19,0xF9,0x03,0x51,0xFA,
|
|
||||||
0x06,0x51,0xFA,0x09,0x51,0xFA,0x0C,0xFB,0x02,0xF8,0x00,0x0C,
|
|
||||||
0xFF,0x49,0x1D,0xF9,0x04,0x6A,0xFF,0x4B,0x1D,0xF9,0x01,0x35,
|
|
||||||
0xFB,0x02,0xF2,0x01,0x36,0xFB,0x07,0x37,0xF2,0x01,0x38,0xFB,
|
|
||||||
0x02,0x31,0xFB,0xFE,0x32,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,
|
|
||||||
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,
|
|
||||||
0xF9,0x00,0xF5,0x4F,0xFF,0x25,0x1D,0xF9,0x01,0x43,0x44,0x45,
|
|
||||||
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0xFA,0xFC,
|
|
||||||
0x4F,0xFA,0xFE,0x4F,0x4F,0xFA,0x02,0x4F,0xFA,0x04,0xFF,0x25,
|
|
||||||
0x1D,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFA,
|
|
||||||
0xFF,0x4D,0xFA,0xFD,0x4E,0xFA,0xFC,0x4F,0xFA,0xF6,0x4F,0xFA,
|
|
||||||
0xF7,0x4F,0xFA,0xF8,0x4F,0xFA,0xF9,0x4F,0xFA,0xFA,0x4F,0xFA,
|
|
||||||
0xFB,0x4F,0xFA,0xFC,0x4F,0xFA,0xFD,0x4F,0xFA,0xFE,0x4F,0xFA,
|
|
||||||
0xFE,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0x4F,
|
|
||||||
0x4F,0x4F,0xFA,0x01,0x4F,0xFA,0x01,0x4F,0xFA,0x02,0x4F,0xFA,
|
|
||||||
0x02,0x4F,0xFA,0x03,0x4F,0xFA,0x04,0x4F,0xFA,0x05,0x4F,0xFA,
|
|
||||||
0x06,0x4F,0xF8,0x00,0x06,0xFF,0x49,0x1D,0xF9,0x03,0x50,0xFF,
|
|
||||||
0xA1,0x1C,0xF9,0x05,0xFB,0xFC,0x32,0x33,0x34,0xFF,0xA0,0x19,
|
|
||||||
0xF9,0x05,0xFB,0x01,0xFA,0x03,0xF7,0x25,0x1E,0x66,0xFB,0x02,
|
|
||||||
0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,
|
|
||||||
0xFE,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0x66,0xFB,0x02,0xFA,0x03,
|
|
||||||
0x67,0xFB,0xFF,0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,
|
|
||||||
0x00,0x06,0xFF,0x49,0x1D,0xF9,0x05,0xFB,0xFF,0xFA,0xFC,0x66,
|
|
||||||
0xFB,0xFF,0xFA,0x03,0xFB,0xFD,0xFA,0x01,0xF4,0xFB,0x01,0xF2,
|
|
||||||
0x01,0x6B,0xFB,0x02,0x6C,0xF2,0x01,0x6D,0xFF,0xB3,0x1F,0x79,
|
|
||||||
0xFB,0x01,0x7A,0x7B,0xFB,0x02,0x7C,0xFB,0x04,0x7D,0xFB,0x03,
|
|
||||||
0x7E,0xFB,0xFC,0x56,0xF2,0x01,0xF4,0xFB,0xFC,0x74,0xFB,0xFE,
|
|
||||||
0x75,0x76,0x77,0xFF,0xA0,0x19,0xFB,0xFB,0xFF,0xA0,0x19,0xF9,
|
|
||||||
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
|
|
||||||
0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x03,0x7F,0x80,0x81,
|
|
||||||
0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,
|
|
||||||
0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,
|
|
||||||
0xFB,0xFF,0xFB,0x02,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
|
|
||||||
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
|
|
||||||
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x01,0x7F,
|
|
||||||
0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
|
|
||||||
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,
|
|
||||||
0x03,0x7E,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
|
|
||||||
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
|
|
||||||
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFE,0x80,0x81,0x82,
|
|
||||||
0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFF,0xFC,0x1E,0xF9,
|
|
||||||
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
|
|
||||||
0x04,0x7D,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
|
|
||||||
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
|
|
||||||
0x7C,0xFB,0x02,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,
|
|
||||||
0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x02,
|
|
||||||
0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,
|
|
||||||
0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x01,0x81,0x82,0x83,
|
|
||||||
0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,
|
|
||||||
0x7B,0xFB,0x02,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
|
|
||||||
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x01,0x83,0x84,0xFF,0xA0,0x19,
|
|
||||||
0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x84,0xFF,0xA0,0x19,
|
|
||||||
0xF9,0x01,0x79,0xFB,0x01,0x84,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
|
|
||||||
0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFF,0xAF,0x1F,0xF9,0x05,0xFB,
|
|
||||||
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0x72,0x73,0x74,
|
|
||||||
0xFB,0xFC,0x75,0x76,0x77,0xFF,0xA0,0x19,0xF9,0x01,0xF3,0x01,
|
|
||||||
0xE5,0xE5,0xE5,0xE5,0xE5,0xE5,0xE6,0xE7,0xE8,0xFF,0xDA,0x1F,
|
|
||||||
0xF9,0x01,0xFB,0xFB,0xE9,0xEA,0xEB,0xEC,0xED,0xEE,0xEF,0xF0,
|
|
||||||
0x85,0x85,0x86,0x86,0x86,0x30,0xFB,0x01,0x31,0xFB,0xFE,0xF9,
|
|
||||||
0x05,0x32,0xF9,0x01,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
|
|
||||||
0xFB,0xEA,0xEC,0xEE,0xF0,0x86,0xFB,0xFF,0xFF,0xA0,0x19,0xF9,
|
|
||||||
0x01,0xFB,0x04,0xBF,0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xF2,0x03,
|
|
||||||
0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCD,0xCD,0xF3,0x01,
|
|
||||||
0xCD,0xCD,0xC9,0xC6,0xFB,0xFC,0xFF,0xA0,0x19,0xF9,0x05,0xF4,
|
|
||||||
0xFB,0x01,0x6B,0xFB,0x02,0x6C,0xF9,0x01,0x6D,0xFF,0x36,0x20,
|
|
||||||
0xF9,0x01,0xFA,0xFE,0xFB,0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFB,
|
|
||||||
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,0x01,0x72,
|
|
||||||
0xFA,0x01,0x73,0xFB,0xFE,0xFF,0x78,0x19,0xF9,0x05,0xF4,0xFA,
|
|
||||||
0xFE,0xFB,0x01,0xFB,0x02,0x6C,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
|
|
||||||
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
|
|
||||||
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0xFB,
|
|
||||||
0x01,0x6E,0x6E,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,
|
|
||||||
0x01,0x72,0xFA,0x01,0x73,0x74,0xFB,0xFC,0x75,0x76,0x77,0xFF,
|
|
||||||
0xA0,0x19,0xF9,0x05,0xF4,0xFA,0xFE,0xFB,0x03,0xB9,0xF6,0xB9,
|
|
||||||
0xFF,0xA5,0x20,0xF9,0x05,0xFF,0xAE,0x20,0xF9,0x01,0xF6,0xB3,
|
|
||||||
0xB4,0xB5,0xB6,0xFB,0x01,0xB7,0xFB,0xFC,0xB9,0xFF,0xBA,0x20,
|
|
||||||
0xF9,0x01,0xF4,0xFB,0x04,0xB1,0xF6,0xB1,0xFF,0xC5,0x20,0xF9,
|
|
||||||
0x01,0xB2,0xF6,0xB2,0xFF,0xCD,0x20,0xFF,0x5A,0x20,0xF8,0x00,
|
|
||||||
0x00,0xF9,0x04,0xB9,0xFF,0xD9,0x20,0xF9,0x05,0xFB,0xFB,0xFA,
|
|
||||||
0xFF,0xF2,0x01,0xD9,0xDA,0xDB,0xFB,0x01,0xDC,0xFB,0xFC,0xFA,
|
|
||||||
0xFD,0xF2,0x01,0xDD,0xFB,0xFC,0xFA,0xFE,0xDE,0xFB,0xFE,0xFA,
|
|
||||||
0xFD,0xDF,0xFB,0xFD,0xFA,0xF8,0xF2,0x04,0xF2,0x01,0xE0,0xFB,
|
|
||||||
0xFF,0xFA,0xFF,0xE1,0xFB,0xFD,0xFA,0xFC,0xE2,0xFB,0xFF,0xFA,
|
|
||||||
0xFB,0xF2,0x01,0xE3,0xFB,0xFE,0xFA,0xFF,0xE4,0x00,0xF2,0x01,
|
|
||||||
0x00,0x00,0x00,0xF2,0x01,0x00,0x00,0x00,0xF2,0x01,0x00,0x00,
|
|
||||||
0x00,0xF2,0x01,0xF1,0x00,0xFF,0x2E,0x21,0x36,0xFF,0x32,0x21,
|
|
||||||
0x55,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x44,
|
|
||||||
0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x53,0x54,0x4C,0xFF,0x4B,
|
|
||||||
0x21,0xFB,0x01,0x30,0xFB,0x02,0x31,0xFB,0x06,0x32,0xFB,0x01,
|
|
||||||
0x33,0xFB,0xFF,0x34,0xFB,0x01,0x35,0xFB,0x01,0xFF,0x51,0x21,
|
|
||||||
0xFB,0x01,0x37,0x38,0xFF,0x32,0x21,0x4D,0x4E,0x4F,0x50,0x51,
|
|
||||||
0x52,0xFB,0x01,0x36,0x36,0x36,0x36,0x36,0x36,0x39,0x3A,0x3B,
|
|
||||||
0x3C,0x3D,0xFB,0x02,0x3E,0xFB,0xFF,0x3F,0xFB,0xFD,0x40,0x41,
|
|
||||||
0xFB,0xFF,0x42,0xFE,0xFB,0x10,0xFB,0x03,0xFF,0x54,0x21,0x0B,
|
|
||||||
0xFF,0x95,0x21,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFE,
|
|
||||||
0xFB,0x08,0x0B,0xFF,0x95,0x21,0xFE,0xFB,0x0B,0x0C,0xFB,0x01,
|
|
||||||
0x0D,0xFB,0x01,0x0E,0xFB,0x03,0x0F,0xFB,0x01,0x10,0x11,0xFF,
|
|
||||||
0xB8,0x21,0x13,0xFF,0xBC,0x21,0x14,0xFF,0xC0,0x21,0x15,0xFB,
|
|
||||||
0x01,0x16,0x17,0x18,0xFB,0x01,0x19,0xFB,0xFD,0x1A,0xFB,0xFE,
|
|
||||||
0x1B,0xFB,0xFC,0x1C,0xFB,0xFD,0x1D,0xFB,0xFE,0x1E,0xFB,0xFD,
|
|
||||||
0x1F,0xFB,0xFF,0x20,0x21,0xFF,0xE2,0x21,0x25,0xFF,0xE6,0x21,
|
|
||||||
0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0xFE,
|
|
||||||
0xFB,0x0C,0x0B,0xFF,0xF8,0x21,0x0B,0x0B,0xFE,0xFB,0x0D,0x2F,
|
|
||||||
0x2E,0x2D,0x2C,0x2B,0x2A,0x29,0x28,0x27,0x26,0x25,0x24,0x24,
|
|
||||||
0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,
|
|
||||||
0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,
|
|
||||||
0x22,0x22,0x23,0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,
|
|
||||||
0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,0x23,0x23,0x24,0xFF,
|
|
||||||
0x3C,0x22,0x01,0x12,0xFF,0x41,0x22,0xF9,0x01,0xBA,0xFB,0x05,
|
|
||||||
0xBA,0xFB,0x03,0xBB,0xFB,0x04,0xFF,0x47,0x22,0xBA,0xFF,0x53,
|
|
||||||
0x22,0xBC,0xFF,0x57,0x22,0xF9,0x00,0xBA,0xBA,0xBA,0xBC,0xBC,
|
|
||||||
0xBC,0xBC,0xBC,0xBC,0xBC,0xBC,0xFE,0xFB,0x08,0xFF,0x47,0x22,
|
|
||||||
0xBA,0xBA,0xBC,0xFF,0x70,0x22,
|
|
||||||
};
|
|
||||||
|
|
||||||
#define KID_NSEQ 115
|
#define KID_NSEQ 115
|
||||||
static const uint16_t kid_seq_off[KID_NSEQ] = {
|
static const uint16_t kid_seq_off[KID_NSEQ] = {
|
||||||
@@ -471,4 +42,31 @@ static const uint16_t kid_seq_off[KID_NSEQ] = {
|
|||||||
0x21FC,0x2240,0x2257,
|
0x21FC,0x2240,0x2257,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.
|
||||||
|
* Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.
|
||||||
|
* Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),
|
||||||
|
* x — «вперёд» по направлению персонажа. */
|
||||||
|
#define KID_NSWORD 51
|
||||||
|
#define KID_SWORD_ID_INIT { \
|
||||||
|
0xFF,0x00,0x05,0x01,0x02,0x06,0x07,0x03,0x04,0x1E,0x08,0x09, \
|
||||||
|
0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x13,0x11, \
|
||||||
|
0x11,0x07,0x01,0x1C,0x08,0x04,0x04,0x0A,0x0D,0x0D,0x0D,0x14, \
|
||||||
|
0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0xFF,0xFF,0x1D,0x08,0x1F, \
|
||||||
|
0x20,0x21,0x08, \
|
||||||
|
}
|
||||||
|
#define KID_SWORD_DX_INIT { \
|
||||||
|
0,0,-9,7,17,7,0,17,16,12,13,7, \
|
||||||
|
10,10,22,28,13,0,0,21,14,21,22,7, \
|
||||||
|
15,0,7,14,7,6,9,11,24,19,21,7, \
|
||||||
|
14,14,14,28,28,21,14,14,21,0,8,14, \
|
||||||
|
14,7,8, \
|
||||||
|
}
|
||||||
|
#define KID_SWORD_DY_INIT { \
|
||||||
|
0,-9,-29,-25,-26,-14,-5,-16,-19,-9,-34,-25, \
|
||||||
|
-16,-11,-21,-23,-35,-38,-29,-19,-23,-22,-23,-13, \
|
||||||
|
-18,-8,-27,-28,-27,-23,-21,-18,-23,-23,-23,-32, \
|
||||||
|
-32,-31,-29,-28,-28,-25,-22,-25,-25,-16,-37,-24, \
|
||||||
|
-24,-14,-37, \
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -14,8 +14,12 @@
|
|||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include "pop_bg.h"
|
#include "pop_bg.h"
|
||||||
|
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
|
||||||
|
#include "pop_state.h"
|
||||||
|
#include "pop_geom.h"
|
||||||
#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */
|
#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */
|
||||||
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */
|
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */
|
||||||
|
#include "pop_guard.h" /* SWORD_2_DRAWN — расширение футпринта под клинок */
|
||||||
|
|
||||||
/* ---- Атласы (прямая адресация) ------------------------------------- */
|
/* ---- Атласы (прямая адресация) ------------------------------------- */
|
||||||
static atlas_t env[5];
|
static atlas_t env[5];
|
||||||
@@ -24,22 +28,25 @@ static atlas_t fore_at;
|
|||||||
static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */
|
static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */
|
||||||
|
|
||||||
static const char *const ENV_ATL[5] = {
|
static const char *const ENV_ATL[5] = {
|
||||||
"pop_env0.atl", "pop_env1.atl", "pop_env2.atl",
|
/* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон,
|
||||||
"pop_env3.atl", "pop_env4.atl"
|
* KID — персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска
|
||||||
|
* зарастает десятками .atl. */
|
||||||
|
"BG\\pop_env0.atl", "BG\\pop_env1.atl", "BG\\pop_env2.atl",
|
||||||
|
"BG\\pop_env3.atl", "BG\\pop_env4.atl"
|
||||||
};
|
};
|
||||||
|
|
||||||
int pop_bg_load(void)
|
int pop_bg_load(void) __banked
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
for (i = 0; i < 5; i++)
|
for (i = 0; i < 5; i++)
|
||||||
if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1;
|
if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1;
|
||||||
if (atlas_load(&wall_at, "pop_wall.atl") != 0) return -1;
|
if (atlas_load(&wall_at, "BG\\pop_wall.atl") != 0) return -1;
|
||||||
if (atlas_load(&fore_at, "pop_fore.atl") != 0) return -1;
|
if (atlas_load(&fore_at, "BG\\pop_fore.atl") != 0) return -1;
|
||||||
if (atlas_load(&pot_at, "pop_pot.atl") != 0) return -1;
|
if (atlas_load(&pot_at, "BG\\pop_pot.atl") != 0) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_bg_free(void)
|
void pop_bg_free(void) __banked
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
for (i = 0; i < 5; i++)
|
for (i = 0; i < 5; i++)
|
||||||
@@ -58,50 +65,84 @@ static int g_clip_top;
|
|||||||
/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x,
|
/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x,
|
||||||
* прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0).
|
* прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0).
|
||||||
* При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */
|
* При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */
|
||||||
|
/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока оно включено,
|
||||||
|
* blit_b рисует только ту часть куска, что попадает в окно.
|
||||||
|
*
|
||||||
|
* Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов
|
||||||
|
* поверх нарисованного персонажа — за пределами его прямоугольника в
|
||||||
|
* видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ:
|
||||||
|
* стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход
|
||||||
|
* до девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход
|
||||||
|
* поверх Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей
|
||||||
|
* работы кадра при бюджете 430 000 тактов на кадр. С окном большинство
|
||||||
|
* кусков отсекается целиком, остальные режутся до пересечения. */
|
||||||
|
static int fclip_x0, fclip_y0, fclip_x1, fclip_y1;
|
||||||
|
static uint8_t fclip_on;
|
||||||
|
|
||||||
|
void pop_fore_set_clip(int x, int y, int w, int h) __banked
|
||||||
|
{
|
||||||
|
fclip_x0 = x; fclip_y0 = y;
|
||||||
|
fclip_x1 = x + w; fclip_y1 = y + h;
|
||||||
|
}
|
||||||
|
|
||||||
static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
|
static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
|
||||||
{
|
{
|
||||||
const uint8_t *img;
|
const uint8_t *img;
|
||||||
uint16_t w, h;
|
uint16_t w, h;
|
||||||
int top;
|
int top, dx, dy, dw, dh, sx = 0, sy = 0;
|
||||||
if (idx >= a->count)
|
if (idx >= a->count)
|
||||||
return;
|
return;
|
||||||
|
/* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
|
||||||
|
* ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
|
||||||
|
* gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее
|
||||||
|
* 64×64) и, если он не задевает окно, уходим не заплатив ни за
|
||||||
|
* atlas_image, ни за map/unmap. */
|
||||||
|
if (fclip_on &&
|
||||||
|
(x >= fclip_x1 || x + 64 <= fclip_x0 ||
|
||||||
|
ybottom + POP_YOFF + 1 <= fclip_y0 ||
|
||||||
|
ybottom + POP_YOFF - 64 >= fclip_y1))
|
||||||
|
return;
|
||||||
img = (const uint8_t *)atlas_image(a, idx);
|
img = (const uint8_t *)atlas_image(a, idx);
|
||||||
gfx_w0_map(a->page);
|
gfx_w0_map(a->page);
|
||||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||||
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
|
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
|
||||||
if (w && h) {
|
dx = x; dy = top; dw = (int)w; dh = (int)h;
|
||||||
if (g_clip_top && top < g_clip_top) {
|
if (g_clip_top && dy < g_clip_top) { /* полоса у потолка */
|
||||||
int sy = g_clip_top - top; /* пропустить верхние строки */
|
sy = g_clip_top - dy; dy = g_clip_top; dh -= sy;
|
||||||
if (sy < (int)h)
|
}
|
||||||
gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy);
|
if (fclip_on) { /* окно fore-слоя */
|
||||||
} else if (x >= 0 && top >= 0 && w < 256 && h < 256 &&
|
if (dx < fclip_x0) { int d = fclip_x0 - dx; sx += d; dx += d; dw -= d; }
|
||||||
x + (int)w <= 320 && top + (int)h <= 256) {
|
if (dy < fclip_y0) { int d = fclip_y0 - dy; sy += d; dy += d; dh -= d; }
|
||||||
/* Спрайт целиком на экране — линейное ядро без клипа: ~2.9×
|
if (dx + dw > fclip_x1) dw = fclip_x1 - dx;
|
||||||
* быстрее (замер: 4617 против 13288 тактов на спрайт 32×3;
|
if (dy + dh > fclip_y1) dh = fclip_y1 - dy;
|
||||||
* общее ядро gfx_blit платит за клип/16-бит/split, а не за
|
}
|
||||||
* пиксели). W3-скобку ставит сама libbgi: из кода, собранного
|
if (w && h && dw > 0 && dh > 0) {
|
||||||
* с --w3, её вызывать нельзя (окно W3 уходит под видеобанк). */
|
/* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее
|
||||||
gfx_blit_noclip(x, top, img);
|
* (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро
|
||||||
|
* gfx_blit платит за клип/16-бит/split, а не за пиксели).
|
||||||
|
* W3-скобку ставит сама libbgi: из кода, собранного с --w3, её
|
||||||
|
* вызывать нельзя (окно W3 уходит под видеобанк). */
|
||||||
|
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
|
||||||
|
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
|
||||||
|
if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h)
|
||||||
|
gfx_blit_noclip(dx, dy, img);
|
||||||
|
else
|
||||||
|
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
|
||||||
|
(uint8_t)dw, (uint8_t)dh);
|
||||||
} else {
|
} else {
|
||||||
gfx_blit(x, top, img); /* край экрана — с клипом */
|
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */
|
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose.
|
||||||
|
* Выбор ядра (noclip / клипающее) — общий с Кидом и стражем, pop_heal_fast
|
||||||
|
* в pop_bg.h. */
|
||||||
static void heal_off(int x, int y, int w, int h)
|
static void heal_off(int x, int y, int w, int h)
|
||||||
{
|
{
|
||||||
int sy = y + POP_YOFF;
|
pop_heal_fast(x, y + POP_YOFF, w, h);
|
||||||
/* Без клипа — линейным ядром (~2.9×, как gfx_blit_noclip): общий
|
|
||||||
* gfx_heal платит за клип/16-бит/split, а не за пиксели (замер:
|
|
||||||
* 11 658 тактов на heal 22×22). */
|
|
||||||
if (x >= 0 && sy >= 0 && w > 0 && h > 0 && w < 256 && h < 256 &&
|
|
||||||
x + w <= 320 && sy + h <= 256)
|
|
||||||
gfx_heal_noclip(x, sy, (uint8_t)w, (uint8_t)h);
|
|
||||||
else
|
|
||||||
gfx_heal(x, sy, w, h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а
|
/* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а
|
||||||
@@ -125,7 +166,7 @@ static void ov_mark(int x, int y, int w, int h)
|
|||||||
ov_pending = 3; /* обе страницы */
|
ov_pending = 3; /* обе страницы */
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_fore_heal(void)
|
void pop_fore_heal(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t pg = gfx_get_draw_page() & 1;
|
uint8_t pg = gfx_get_draw_page() & 1;
|
||||||
if (!(ov_pending & (1 << pg))) return;
|
if (!(ov_pending & (1 << pg))) return;
|
||||||
@@ -249,12 +290,12 @@ static uint8_t g_room;
|
|||||||
* floorright, когда тайл нижнего ряда стал видимым (empty после падения
|
* floorright, когда тайл нижнего ряда стал видимым (empty после падения
|
||||||
* loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты
|
* loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты
|
||||||
* снизу (кромка уровня = стена). */
|
* снизу (кромка уровня = стена). */
|
||||||
void pop_room_set_below(const uint8_t *below_row0_fg) { g_below = below_row0_fg; }
|
void pop_room_set_below(const uint8_t *below_row0_fg) __banked { g_below = below_row0_fg; }
|
||||||
|
|
||||||
/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки
|
/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки
|
||||||
* у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
|
* у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
|
||||||
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
|
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
|
||||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod)
|
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
|
||||||
{ g_above = above_row2_fg; g_abovemod = above_row2_mod; }
|
{ g_above = above_row2_fg; g_abovemod = above_row2_mod; }
|
||||||
|
|
||||||
static uint8_t tile_code(int row, int col)
|
static uint8_t tile_code(int row, int col)
|
||||||
@@ -309,13 +350,9 @@ static uint8_t wall_modifier(int row, int col)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */
|
/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */
|
||||||
static unsigned long rnd_seed;
|
static pop_rnd_t rnd_seed;
|
||||||
|
|
||||||
static uint16_t prandom(uint16_t maxv)
|
#define prandom(maxv) pop_prandom(&rnd_seed, (maxv))
|
||||||
{
|
|
||||||
rnd_seed = rnd_seed * 214013UL + 2531011UL;
|
|
||||||
return (uint16_t)((uint16_t)(rnd_seed >> 16) % (uint16_t)(maxv + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */
|
/* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */
|
||||||
static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col)
|
static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col)
|
||||||
@@ -338,65 +375,135 @@ static void draw_right_mark(uint16_t dv, int arg1, int row, int col)
|
|||||||
wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]);
|
wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
|
||||||
|
/* КЭШ РАСКЛАДКИ КЛАДКИ. Серия prandom для тайла зависит только от
|
||||||
|
* (комната, ряд, колонка) — пока мы в комнате, это КОНСТАНТА. А
|
||||||
|
* wall_pattern зовётся из fore-слоя КАЖДЫЙ кадр, и на Z80 это самая
|
||||||
|
* дорогая вещь в кадре: замер в MAME 2026-07-29 (комната 3) — 18 вызовов
|
||||||
|
* pop_prandom за один fore-проход, ~10 700 тактов каждый (32-битный LCG:
|
||||||
|
* __mullong ~8 000 + __moduint), то есть 193 К из 341 К всего прохода при
|
||||||
|
* бюджете кадра ~430 К. Кэшируем не сид, а ГОТОВЫЕ РЕШЕНИЯ (что рисовать
|
||||||
|
* и с каким смещением) — тогда в кадре не остаётся ни одного prandom.
|
||||||
|
*
|
||||||
|
* Порядок вызовов prandom воспроизведён один в один, включая то, что
|
||||||
|
* значение метки берётся ТОЛЬКО когда сработало её условие: иначе
|
||||||
|
* последовательность разъедется и кладка станет другой. which_part=0
|
||||||
|
* использует префикс той же серии, поэтому кэш общий.
|
||||||
|
*
|
||||||
|
* Упаковка: wp_a = mid_div | mid_off<<1 | bot_div<<4 | bot_off<<5;
|
||||||
|
* wp_b = rndblk | right<<1 | right_val<<2 | left<<4 | left_val<<5.
|
||||||
|
* Индекс — (row+1)*10+col, ряды −1..2. */
|
||||||
|
#define WP_N 40
|
||||||
|
static uint8_t wp_a[WP_N], wp_b[WP_N], wp_ok[WP_N];
|
||||||
|
|
||||||
|
static void wall_pattern_reset(void) /* смена комнаты — сид другой */
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < WP_N; i++) wp_ok[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Выходы wall_rnd — в файловых статиках: возвращать девять значений
|
||||||
|
* через указатели на Z80 дороже самой функции. */
|
||||||
|
static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off;
|
||||||
|
static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv;
|
||||||
|
|
||||||
|
static void wall_rnd(int row, int col)
|
||||||
|
{
|
||||||
|
/* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс
|
||||||
|
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
|
||||||
|
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
|
||||||
|
uint8_t a, b, bg;
|
||||||
|
pop_rnd_t saved;
|
||||||
|
if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
|
||||||
|
a = wp_a[idx]; b = wp_b[idx];
|
||||||
|
wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7);
|
||||||
|
wr_bot_div = (uint8_t)((a >> 4) & 1); wr_bot_off = (uint8_t)((a >> 5) & 7);
|
||||||
|
wr_rndblk = (uint8_t)(b & 1);
|
||||||
|
wr_right = (uint8_t)((b >> 1) & 1); wr_rightv = (uint8_t)((b >> 2) & 3);
|
||||||
|
wr_left = (uint8_t)((b >> 4) & 1); wr_leftv = (uint8_t)((b >> 5) & 7);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saved = rnd_seed;
|
||||||
|
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
|
||||||
|
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
|
||||||
|
pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col);
|
||||||
|
prandom(1); /* сброс */
|
||||||
|
wr_mid_div = (uint8_t)prandom(1);
|
||||||
|
wr_mid_off = (uint8_t)prandom(4);
|
||||||
|
wr_bot_div = (uint8_t)prandom(1);
|
||||||
|
wr_bot_off = (uint8_t)prandom(4);
|
||||||
|
wr_rndblk = wr_right = wr_left = 0;
|
||||||
|
wr_rightv = wr_leftv = 0;
|
||||||
|
bg = wall_modifier(row, col);
|
||||||
|
if (bg == 3) { /* WWW */
|
||||||
|
wr_rndblk = (uint8_t)(prandom(4) == 0);
|
||||||
|
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
|
||||||
|
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
|
||||||
|
} else if (bg == 0) { /* SWS */
|
||||||
|
if (prandom(6) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(1); }
|
||||||
|
} else if (bg == 1) { /* SWW */
|
||||||
|
wr_rndblk = (uint8_t)(prandom(4) == 0);
|
||||||
|
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
|
||||||
|
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(3); }
|
||||||
|
} else { /* WWS (bg==2) */
|
||||||
|
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(1) + 2); }
|
||||||
|
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
|
||||||
|
}
|
||||||
|
rnd_seed = saved;
|
||||||
|
if (idx >= 0 && idx < WP_N) {
|
||||||
|
wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) |
|
||||||
|
(wr_bot_div << 4) | (wr_bot_off << 5));
|
||||||
|
wp_b[idx] = (uint8_t)(wr_rndblk | (wr_right << 1) | (wr_rightv << 2) |
|
||||||
|
(wr_left << 4) | (wr_leftv << 5));
|
||||||
|
wp_ok[idx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
|
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
|
||||||
static void wall_pattern(int row, int col, int which_part)
|
static void wall_pattern(int row, int col, int which_part)
|
||||||
{
|
{
|
||||||
int xh = COL_XH[col];
|
int xh = COL_XH[col];
|
||||||
int dby = 63 * row + 65;
|
int dby = 63 * row + 65;
|
||||||
unsigned long saved = rnd_seed;
|
uint8_t bg = wall_modifier(row, col);
|
||||||
uint16_t mid_div, mid_off, bot_div, bot_off;
|
|
||||||
uint8_t bg;
|
|
||||||
|
|
||||||
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
|
|
||||||
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
|
|
||||||
rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col);
|
|
||||||
prandom(1); /* сброс */
|
|
||||||
mid_div = prandom(1);
|
|
||||||
mid_off = prandom(4);
|
|
||||||
bot_div = prandom(1);
|
|
||||||
bot_off = prandom(4);
|
|
||||||
bg = wall_modifier(row, col);
|
|
||||||
|
|
||||||
|
wall_rnd(row, col);
|
||||||
if (bg == 3) { /* WWW */
|
if (bg == 3) { /* WWW */
|
||||||
if (which_part) {
|
if (which_part) {
|
||||||
if (prandom(4) == 0)
|
if (wr_rndblk)
|
||||||
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
|
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
|
||||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
|
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
|
||||||
}
|
}
|
||||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby);
|
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
|
||||||
if (which_part) {
|
if (which_part) {
|
||||||
if (prandom(4) == 0)
|
if (wr_right)
|
||||||
draw_right_mark(prandom(3), mid_off, row, col);
|
draw_right_mark(wr_rightv, wr_mid_off, row, col);
|
||||||
if (prandom(4) == 0)
|
if (wr_left)
|
||||||
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col);
|
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||||
}
|
}
|
||||||
} else if (bg == 0) { /* SWS */
|
} else if (bg == 0) { /* SWS */
|
||||||
if (which_part) {
|
if (which_part && wr_left)
|
||||||
if (prandom(6) == 0)
|
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||||
draw_left_mark(prandom(1), mid_off - mid_div, bot_off - bot_div, row, col);
|
|
||||||
}
|
|
||||||
} else if (bg == 1) { /* SWW */
|
} else if (bg == 1) { /* SWW */
|
||||||
if (which_part) {
|
if (which_part) {
|
||||||
if (prandom(4) == 0)
|
if (wr_rndblk)
|
||||||
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
|
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
|
||||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
|
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
|
||||||
if (prandom(4) == 0)
|
if (wr_right)
|
||||||
draw_right_mark(prandom(3), mid_off, row, col);
|
draw_right_mark(wr_rightv, wr_mid_off, row, col);
|
||||||
if (prandom(4) == 0)
|
if (wr_left)
|
||||||
draw_left_mark(prandom(3), mid_off - mid_div, bot_off - bot_div, row, col);
|
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||||
}
|
}
|
||||||
} else { /* WWS (bg==2) */
|
} else { /* WWS (bg==2) */
|
||||||
if (which_part)
|
if (which_part)
|
||||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
|
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
|
||||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby);
|
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
|
||||||
if (which_part) {
|
if (which_part) {
|
||||||
if (prandom(4) == 0)
|
if (wr_right)
|
||||||
draw_right_mark((uint16_t)(prandom(1) + 2), mid_off, row, col);
|
draw_right_mark(wr_rightv, wr_mid_off, row, col);
|
||||||
if (prandom(4) == 0)
|
if (wr_left)
|
||||||
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col);
|
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rnd_seed = saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* topright тайла снизу-слева (seg008.c:410). */
|
/* topright тайла снизу-слева (seg008.c:410). */
|
||||||
@@ -479,6 +586,11 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy)
|
|||||||
int ybottom = dmy - 13;
|
int ybottom = dmy - 13;
|
||||||
int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */
|
int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */
|
||||||
int y = ybottom - (int)modif;
|
int y = ybottom - (int)modif;
|
||||||
|
/* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО
|
||||||
|
* тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из
|
||||||
|
* draw_tile_anim_right, разбирая левого соседа). +8 у palace-уровней
|
||||||
|
* (tbl_level_type) — на уровне 1 не применяется. */
|
||||||
|
pop_leveldoor_right = xh * 8 + 48;
|
||||||
env_b(99, x, ybottom);
|
env_b(99, x, ybottom);
|
||||||
if (modif)
|
if (modif)
|
||||||
env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */
|
env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */
|
||||||
@@ -487,6 +599,9 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy)
|
|||||||
if (y > ylo) ylo += 4;
|
if (y > ylo) ylo += 4;
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
|
/* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое
|
||||||
|
* значение (низ поднятой створки), а не стартовое. */
|
||||||
|
pop_leveldoor_ybottom = ylo;
|
||||||
env_b(34, x, dmy - 64);
|
env_b(34, x, dmy - 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +633,11 @@ static void draw_tile(int row, int col)
|
|||||||
* room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */
|
* room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */
|
||||||
if (row + 1 > 2) {
|
if (row + 1 > 2) {
|
||||||
int lc = col - 1;
|
int lc = col - 1;
|
||||||
if (lc < 0) rbl_code = 20; /* кромка уровня = стена */
|
/* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате
|
||||||
|
* (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда;
|
||||||
|
* «стена» там стоит, только если такой комнаты нет — как в
|
||||||
|
* load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */
|
||||||
|
if (lc < 0) rbl_code = g_below ? (uint8_t)(g_below[10] & 0x1F) : 20;
|
||||||
else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1;
|
else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1;
|
||||||
} else rbl_code = tile_code(row + 1, col - 1);
|
} else rbl_code = tile_code(row + 1, col - 1);
|
||||||
|
|
||||||
@@ -636,7 +755,7 @@ static void draw_tile(int row, int col)
|
|||||||
* Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
|
* Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
|
||||||
* ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif
|
* ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif
|
||||||
* внутри draw_tile). */
|
* внутри draw_tile). */
|
||||||
void pop_loose_shake_draw(int row, int col)
|
void pop_loose_shake_draw(int row, int col) __banked
|
||||||
{
|
{
|
||||||
int x = COL_XH[col] * 8;
|
int x = COL_XH[col] * 8;
|
||||||
/* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
|
/* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
|
||||||
@@ -658,7 +777,7 @@ void pop_loose_shake_draw(int row, int col)
|
|||||||
* читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать
|
* читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать
|
||||||
* на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile.
|
* на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile.
|
||||||
* heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */
|
* heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */
|
||||||
void pop_spike_redraw(int row, int col)
|
void pop_spike_redraw(int row, int col) __banked
|
||||||
{
|
{
|
||||||
int x = COL_XH[col] * 8;
|
int x = COL_XH[col] * 8;
|
||||||
/* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ
|
/* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ
|
||||||
@@ -685,7 +804,7 @@ void pop_spike_redraw(int row, int col)
|
|||||||
* room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake):
|
* room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake):
|
||||||
* heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары
|
* heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары
|
||||||
* оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
|
* оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
|
||||||
void pop_room_redraw_seam_left(void)
|
void pop_room_redraw_seam_left(void) __banked
|
||||||
{
|
{
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
setfillstyle(SOLID_FILL, BLACK);
|
setfillstyle(SOLID_FILL, BLACK);
|
||||||
@@ -707,7 +826,7 @@ void pop_room_redraw_seam_left(void)
|
|||||||
* после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
|
* после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
|
||||||
#define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */
|
#define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */
|
||||||
|
|
||||||
void pop_ceil_shake_draw(int col)
|
void pop_ceil_shake_draw(int col) __banked
|
||||||
{
|
{
|
||||||
int x = COL_XH[col] * 8;
|
int x = COL_XH[col] * 8;
|
||||||
heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */
|
heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */
|
||||||
@@ -719,7 +838,7 @@ void pop_ceil_shake_draw(int col)
|
|||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_ceil_bake_empty(int col)
|
void pop_ceil_bake_empty(int col) __banked
|
||||||
{
|
{
|
||||||
int x = COL_XH[col] * 8;
|
int x = COL_XH[col] * 8;
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
@@ -728,8 +847,22 @@ void pop_ceil_bake_empty(int col)
|
|||||||
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
||||||
g_clip_top = POP_YOFF;
|
g_clip_top = POP_YOFF;
|
||||||
bake_rest = 1; /* фон = покой (см. bake_rest) */
|
bake_rest = 1; /* фон = покой (см. bake_rest) */
|
||||||
draw_tile(-1, col); /* тайл уже EMPTY → колодец наверх */
|
/* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только
|
||||||
if (col + 1 < 10) draw_tile(-1, col + 1);
|
* два тайла ряда −1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей
|
||||||
|
* нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут:
|
||||||
|
* • ряд −1, колонки col−1..col+1 — сама полоса кладки у потолка;
|
||||||
|
* • ряд 0, колонки col−1..col+1 — их верхушки заходят в те же 8 px.
|
||||||
|
* Раньше перерисовывались только (−1,col) и (−1,col+1), и в полосе
|
||||||
|
* оставался чёрный прямоугольник ~13×3 px (замер: экранные y 47..52 при
|
||||||
|
* полосе 44..52), который heal возвращал каждый кадр. Признак, что дело
|
||||||
|
* именно в запечке: после выхода из комнаты и возврата бара нет. */
|
||||||
|
{
|
||||||
|
int c0 = (col > 0) ? col - 1 : 0;
|
||||||
|
int c1 = (col + 1 < 9) ? col + 1 : 9;
|
||||||
|
int c;
|
||||||
|
for (c = c0; c <= c1; c++) draw_tile(-1, c);
|
||||||
|
for (c = c0; c <= c1; c++) draw_tile(0, c);
|
||||||
|
}
|
||||||
bake_rest = 0;
|
bake_rest = 0;
|
||||||
g_clip_top = 0;
|
g_clip_top = 0;
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
@@ -742,7 +875,7 @@ void pop_ceil_bake_empty(int col)
|
|||||||
* draw_main_y − 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька
|
* draw_main_y − 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька
|
||||||
* potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y −
|
* potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y −
|
||||||
* (pot_size<<2) − 14 (seg008:668). */
|
* (pot_size<<2) − 14 (seg008:668). */
|
||||||
void pop_torch_draw(int row, int col, uint8_t modif)
|
void pop_torch_draw(int row, int col, uint8_t modif) __banked
|
||||||
{
|
{
|
||||||
/* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1
|
/* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1
|
||||||
* (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env
|
* (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env
|
||||||
@@ -759,7 +892,7 @@ void pop_torch_draw(int row, int col, uint8_t modif)
|
|||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_potion_draw(int row, int col, uint8_t modif)
|
void pop_potion_draw(int row, int col, uint8_t modif) __banked
|
||||||
{
|
{
|
||||||
uint8_t type = potion_type(modif);
|
uint8_t type = potion_type(modif);
|
||||||
int pot_size = (type >= 2 && type < 5) ? 1 : 0;
|
int pot_size = (type >= 2 && type < 5) ? 1 : 0;
|
||||||
@@ -777,7 +910,7 @@ void pop_potion_draw(int row, int col, uint8_t modif)
|
|||||||
* (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный +
|
* (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный +
|
||||||
* redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
|
* redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
|
||||||
* в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */
|
* в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */
|
||||||
void pop_floor_bake(int row, int col)
|
void pop_floor_bake(int row, int col) __banked
|
||||||
{
|
{
|
||||||
int x = COL_XH[col] * 8;
|
int x = COL_XH[col] * 8;
|
||||||
int yb = POP_YOFF + 63 * row;
|
int yb = POP_YOFF + 63 * row;
|
||||||
@@ -795,26 +928,55 @@ void pop_floor_bake(int row, int col)
|
|||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_button_redraw(int row, int col) { pop_floor_bake(row, col); }
|
void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); }
|
||||||
|
|
||||||
/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка,
|
/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка,
|
||||||
* см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет,
|
* см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет,
|
||||||
* а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое.
|
* а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое.
|
||||||
* Куски двери непрозрачные — wipe не нужен. */
|
* Куски двери непрозрачные — wipe не нужен. */
|
||||||
void pop_leveldoor_redraw(int row, int col)
|
void pop_leveldoor_redraw(int row, int col) __banked
|
||||||
{
|
{
|
||||||
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
||||||
draw_tile(row, col);
|
draw_tile(row, col);
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob,
|
||||||
|
* seg007:01E6 — им заканчивается animate_door). (row,col) — сами ворота;
|
||||||
|
* решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому
|
||||||
|
* wipe и redraw идут по ней. Логика ровно как у левого шва
|
||||||
|
* (pop_room_redraw_seam_left), только колонка произвольная.
|
||||||
|
*
|
||||||
|
* Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при
|
||||||
|
* ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары. Полоса — ровно
|
||||||
|
* зона решётки: от gate_top_y (dby−62 = 63*row+3) до низа (63*row+64);
|
||||||
|
* выше начинается ряд −1, ниже — стена ряда ниже.
|
||||||
|
*
|
||||||
|
* Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их
|
||||||
|
* не видно — оригинал такой trob тоже пропускает
|
||||||
|
* (get_trob_right_pos_in_drawn_room → 30). Для взгляда из комнаты справа
|
||||||
|
* работает pop_room_redraw_seam_left. */
|
||||||
|
void pop_gate_redraw(int row, int col) __banked
|
||||||
|
{
|
||||||
|
int x, yt;
|
||||||
|
if (col + 1 > 9 || row < 0 || row > 2) return;
|
||||||
|
x = COL_XH[col + 1] * 8;
|
||||||
|
yt = POP_YOFF + 63 * row;
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
setfillstyle(SOLID_FILL, BLACK);
|
||||||
|
bar(x, yt + 3, x + 31, yt + 64);
|
||||||
|
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
||||||
|
draw_tile(row, col + 1);
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт
|
/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт
|
||||||
* redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И
|
* redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И
|
||||||
* тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose —
|
* тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose —
|
||||||
* рисовал loose-правый-край env_b(42); теперь пусто → чистая грань).
|
* рисовал loose-правый-край env_b(42); теперь пусто → чистая грань).
|
||||||
* g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые
|
* g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые
|
||||||
* пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
|
* пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
|
||||||
void pop_loose_bake_empty(int row, int col)
|
void pop_loose_bake_empty(int row, int col) __banked
|
||||||
{
|
{
|
||||||
int x = COL_XH[col] * 8;
|
int x = COL_XH[col] * 8;
|
||||||
int xr = COL_XH[col + 1] * 8;
|
int xr = COL_XH[col + 1] * 8;
|
||||||
@@ -872,12 +1034,14 @@ typedef struct {
|
|||||||
int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */
|
int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */
|
||||||
int8_t speed;
|
int8_t speed;
|
||||||
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
|
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
|
||||||
|
uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
|
||||||
} mob_t;
|
} mob_t;
|
||||||
static mob_t mobs[MOB_MAX];
|
static mob_t mobs[MOB_MAX];
|
||||||
|
|
||||||
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
|
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
|
||||||
* который он лёг → приложение/pop_map ставит debris + do_knock. */
|
* который он лёг → приложение/pop_map ставит debris + do_knock. */
|
||||||
uint8_t pop_loose_landed;
|
/* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а
|
||||||
|
* писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */
|
||||||
|
|
||||||
/* y_loose_land / y_something (seg007): [row+1], row = −1..3. Ряд −1 —
|
/* y_loose_land / y_something (seg007): [row+1], row = −1..3. Ряд −1 —
|
||||||
* плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный
|
* плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный
|
||||||
@@ -886,7 +1050,7 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
|
|||||||
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
|
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
|
||||||
#define MOB_Y_NONE 0x7FFF
|
#define MOB_Y_NONE 0x7FFF
|
||||||
|
|
||||||
void pop_loose_mob_spawn(int row, int col)
|
void pop_loose_mob_spawn(int row, int col) __banked
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
mob_t *m = 0;
|
mob_t *m = 0;
|
||||||
@@ -899,6 +1063,7 @@ void pop_loose_mob_spawn(int row, int col)
|
|||||||
m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
|
m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
|
||||||
m->row = (int8_t)row;
|
m->row = (int8_t)row;
|
||||||
m->col = (int8_t)col;
|
m->col = (int8_t)col;
|
||||||
|
m->room = g_room; /* curmob.room (add_mob, seg007:1010) */
|
||||||
m->speed = 0;
|
m->speed = 0;
|
||||||
m->active = 1;
|
m->active = 1;
|
||||||
m->clean = 0;
|
m->clean = 0;
|
||||||
@@ -923,7 +1088,7 @@ static int8_t obj_col, obj_row;
|
|||||||
|
|
||||||
static void mob_render(mob_t *m, uint8_t pg);
|
static void mob_render(mob_t *m, uint8_t pg);
|
||||||
|
|
||||||
void pop_loose_mob_reset(void)
|
void pop_loose_mob_reset(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
for (i = 0; i < MOB_MAX; i++) {
|
for (i = 0; i < MOB_MAX; i++) {
|
||||||
@@ -932,14 +1097,42 @@ void pop_loose_mob_reset(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Смена комнаты. Куски НЕ останавливаем: в оригинале mobs[] чистится только
|
||||||
|
* в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо
|
||||||
|
* от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид
|
||||||
|
* не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12
|
||||||
|
* плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только
|
||||||
|
* heal-историю: новая комната перерисована целиком, старых пикселей нет. */
|
||||||
|
void pop_loose_mob_room_changed(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < MOB_MAX; i++) {
|
||||||
|
mobs[i].defer = 0;
|
||||||
|
mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE;
|
||||||
|
/* дочистка хвоста относилась к покинутой комнате — она уже не нужна */
|
||||||
|
if (!mobs[i].active) mobs[i].clean = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки
|
||||||
|
* и соседей), в чужой — прямо из данных уровня. */
|
||||||
|
static uint8_t mob_tile_at(mob_t *m)
|
||||||
|
{
|
||||||
|
if (m->room == g_room) return (uint8_t)(tile_code(m->row, m->col) & 0x1F);
|
||||||
|
if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1; /* кромка = пол */
|
||||||
|
return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F);
|
||||||
|
}
|
||||||
|
|
||||||
static void mob_tick_one(mob_t *m, uint8_t pg)
|
static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||||
{
|
{
|
||||||
|
uint8_t here;
|
||||||
if (!m->active && !m->clean) return;
|
if (!m->active && !m->clean) return;
|
||||||
|
here = (uint8_t)(m->room == g_room);
|
||||||
/* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
|
/* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
|
||||||
* страницы своя копия фона). Раньше коридор был фиксирован под ряд 2;
|
* страницы своя копия фона). Раньше коридор был фиксирован под ряд 2;
|
||||||
* плита-потолок падает через весь ряд 0, поэтому чистим по фактической
|
* плита-потолок падает через весь ряд 0, поэтому чистим по фактической
|
||||||
* прошлой позиции. */
|
* прошлой позиции. */
|
||||||
if (m->prev_y[pg] != MOB_Y_NONE) {
|
if (here && m->prev_y[pg] != MOB_Y_NONE) {
|
||||||
heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
|
heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
|
||||||
m->prev_y[pg] = MOB_Y_NONE;
|
m->prev_y[pg] = MOB_Y_NONE;
|
||||||
}
|
}
|
||||||
@@ -952,9 +1145,17 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
|||||||
* loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
|
* loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
|
||||||
* пол — приземление (loose_land: debris + do_knock). */
|
* пол — приземление (loose_land: debris + do_knock). */
|
||||||
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
|
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
|
||||||
uint8_t t = (uint8_t)(tile_code(m->row, m->col) & 0x1F);
|
uint8_t t = mob_tile_at(m);
|
||||||
if (t == 0 || t == 11) {
|
if (t == 0 || t == 11) {
|
||||||
m->row++; /* mob_down_a_row */
|
m->row++; /* mob_down_a_row */
|
||||||
|
} else if (!here) {
|
||||||
|
/* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, сигнальная
|
||||||
|
* пара pop_loose_landed/pop_debris_at обслуживает только текущую
|
||||||
|
* комнату — щебень кладём прямо в данные уровня (loose_land пишет
|
||||||
|
* в curr_room_tiles комнаты curmob.room, а не отрисованной). */
|
||||||
|
pop_level_set_tile(m->room, (uint8_t)(m->row * 10 + m->col), 0x0E);
|
||||||
|
m->active = 0; m->clean = 0;
|
||||||
|
return;
|
||||||
} else if (pop_loose_landed) {
|
} else if (pop_loose_landed) {
|
||||||
/* сигнал прошлого приземления ещё не разобран (один за кадр) —
|
/* сигнал прошлого приземления ещё не разобран (один за кадр) —
|
||||||
* подержим кусок на границе и приземлим следующим кадром */
|
* подержим кусок на границе и приземлим следующим кадром */
|
||||||
@@ -967,9 +1168,10 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
|
if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
|
||||||
m->active = 0; m->clean = 2;
|
m->active = 0; m->clean = (uint8_t)(here ? 2 : 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!here) return; /* чужая комната — считаем, но не рисуем */
|
||||||
/* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ
|
/* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ
|
||||||
* тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри
|
* тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри
|
||||||
* ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто
|
* ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто
|
||||||
@@ -988,7 +1190,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
|||||||
mob_render(m, pg);
|
mob_render(m, pg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_loose_mob_tick(void)
|
void pop_loose_mob_tick(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t pg = gfx_get_draw_page() & 1, i;
|
uint8_t pg = gfx_get_draw_page() & 1, i;
|
||||||
for (i = 0; i < MOB_MAX; i++)
|
for (i = 0; i < MOB_MAX; i++)
|
||||||
@@ -1038,7 +1240,7 @@ static void mob_render(mob_t *m, uint8_t pg)
|
|||||||
|
|
||||||
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
|
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
|
||||||
* после kid_draw, до fore-слоя. */
|
* после kid_draw, до fore-слоя. */
|
||||||
void pop_loose_mob_draw_over(void)
|
void pop_loose_mob_draw_over(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t pg = gfx_get_draw_page() & 1, i;
|
uint8_t pg = gfx_get_draw_page() & 1, i;
|
||||||
for (i = 0; i < MOB_MAX; i++)
|
for (i = 0; i < MOB_MAX; i++)
|
||||||
@@ -1047,11 +1249,16 @@ void pop_loose_mob_draw_over(void)
|
|||||||
|
|
||||||
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
|
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
|
||||||
* y (низ куска). 0 — куска нет. */
|
* y (низ куска). 0 — куска нет. */
|
||||||
uint8_t pop_loose_mob_pos(int *col, int *y)
|
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
/* check_loose_fall_on_kid (seg007:1192) начинается с Char.room ==
|
||||||
|
* curmob.room: кусок, падающий в покинутой комнате, Киду по голове не
|
||||||
|
* бьёт. */
|
||||||
for (i = 0; i < MOB_MAX; i++)
|
for (i = 0; i < MOB_MAX; i++)
|
||||||
if (mobs[i].active) { *col = mobs[i].col; *y = mobs[i].y; return 1; }
|
if (mobs[i].active && mobs[i].room == g_room) {
|
||||||
|
*col = mobs[i].col; *y = mobs[i].y; return 1;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,7 +1269,7 @@ uint8_t pop_loose_mob_pos(int *col, int *y)
|
|||||||
* кадрах падения — гейт по флагу). */
|
* кадрах падения — гейт по флагу). */
|
||||||
static uint8_t border_dirty;
|
static uint8_t border_dirty;
|
||||||
|
|
||||||
void pop_clip_sprite(int x, int w, int sy_top, int h)
|
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
|
||||||
{
|
{
|
||||||
int sy_bot = sy_top + h;
|
int sy_bot = sy_top + h;
|
||||||
(void)x; (void)w;
|
(void)x; (void)w;
|
||||||
@@ -1074,20 +1281,26 @@ void pop_clip_sprite(int x, int w, int sy_top, int h)
|
|||||||
* вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает
|
* вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает
|
||||||
* чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу
|
* чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу
|
||||||
* + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */
|
* + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */
|
||||||
void pop_room_clip_borders(void)
|
void pop_room_clip_borders(void) __banked
|
||||||
{
|
{
|
||||||
if (!border_dirty) return;
|
if (!border_dirty) return;
|
||||||
border_dirty--;
|
border_dirty--;
|
||||||
|
/* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320
|
||||||
|
* не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска
|
||||||
|
* по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах
|
||||||
|
* падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там
|
||||||
|
* тонет в цене самих 320×28 пикселей. Не «оптимизировать» повторно. */
|
||||||
gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */
|
gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */
|
||||||
gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */
|
gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_room_draw(uint8_t room_num,
|
void pop_room_draw(uint8_t room_num,
|
||||||
const uint8_t *fg, const uint8_t *bg,
|
const uint8_t *fg, const uint8_t *bg,
|
||||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg)
|
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked
|
||||||
{
|
{
|
||||||
int row, col;
|
int row, col;
|
||||||
g_room = room_num;
|
g_room = room_num;
|
||||||
|
wall_pattern_reset(); /* кэш кладки привязан к номеру комнаты */
|
||||||
g_fg = fg; g_bg = bg;
|
g_fg = fg; g_bg = bg;
|
||||||
g_lfg = lcol_fg; g_lbg = lcol_bg;
|
g_lfg = lcol_fg; g_lbg = lcol_bg;
|
||||||
|
|
||||||
@@ -1125,10 +1338,7 @@ static int8_t col_from_x(int xpos) /* get_tile_div_mod: колонка (
|
|||||||
return (int8_t)xh;
|
return (int8_t)xh;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int8_t y_to_row(int y) /* y_to_row_mod4 (seg006, TILE_SIZEY=63) */
|
#define y_to_row(y) pop_y_to_row((int16_t)(y)) /* общий (pop_geom) */
|
||||||
{
|
|
||||||
return (int8_t)((y + 60) / 63 % 4 - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void fore_only_tile(int row, int col)
|
static void fore_only_tile(int row, int col)
|
||||||
{
|
{
|
||||||
@@ -1180,8 +1390,25 @@ static void ceil_over_kid_tile(int col)
|
|||||||
fore_only_tile(-1, col); /* draw_tile_fore */
|
fore_only_tile(-1, col); /* draw_tile_fore */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Пересекается ли габарит тайла с окном fore-слоя. Шаг тайла — 32 px
|
||||||
|
* (COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры
|
||||||
|
* и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра.
|
||||||
|
* Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной
|
||||||
|
* задевает не все тайлы футпринта, а платили за каждый (atlas_image +
|
||||||
|
* gfx_w0_map + разбор заголовка на КАЖДЫЙ кусок). */
|
||||||
|
static uint8_t tile_in_fclip(int row, int col)
|
||||||
|
{
|
||||||
|
int x0, y0;
|
||||||
|
if (!fclip_on) return 1;
|
||||||
|
x0 = COL_XH[col] * 8;
|
||||||
|
y0 = POP_YOFF + 63 * row;
|
||||||
|
return (uint8_t)(x0 < fclip_x1 && x0 + 40 > fclip_x0 &&
|
||||||
|
y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0);
|
||||||
|
}
|
||||||
|
|
||||||
static void fore_tile(int row, int col) /* передний слой одного тайла */
|
static void fore_tile(int row, int col) /* передний слой одного тайла */
|
||||||
{
|
{
|
||||||
|
if (!tile_in_fclip(row, col)) return;
|
||||||
/* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
|
/* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
|
||||||
* кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
|
* кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
|
||||||
* backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ
|
* backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ
|
||||||
@@ -1214,7 +1441,9 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx)
|
|||||||
{
|
{
|
||||||
/* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
|
/* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
|
||||||
* Kid спускается/подтягивается, закрашивается целым тайлом */
|
* Kid спускается/подтягивается, закрашивается целым тайлом */
|
||||||
uint8_t code = tile_code_drawn(row, col);
|
uint8_t code;
|
||||||
|
if (!tile_in_fclip(row, col)) return;
|
||||||
|
code = tile_code_drawn(row, col);
|
||||||
int x, dmy, dby;
|
int x, dmy, dby;
|
||||||
const piece *t;
|
const piece *t;
|
||||||
if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */
|
if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */
|
||||||
@@ -1304,8 +1533,9 @@ static void overlay_mid_tile(int row, int col)
|
|||||||
* сразу), поэтому порядок эмулируется явной проверкой. */
|
* сразу), поэтому порядок эмулируется явной проверкой. */
|
||||||
static void other_overlay_tile(int row, int col)
|
static void other_overlay_tile(int row, int col)
|
||||||
{
|
{
|
||||||
uint8_t code = tile_code(row, col);
|
uint8_t code, left;
|
||||||
uint8_t left;
|
if (!tile_in_fclip(row, col)) return;
|
||||||
|
code = tile_code(row, col);
|
||||||
/* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для
|
/* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для
|
||||||
* него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань
|
* него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань
|
||||||
* пустого соседа). В оригинале она уходит в y-сортируемый midtable и
|
* пустого соседа). В оригинале она уходит в y-сортируемый midtable и
|
||||||
@@ -1328,18 +1558,33 @@ static void other_overlay_tile(int row, int col)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
/* Футпринт персонажа в тайлах (порт set_char_collision, seg006:0723).
|
||||||
int8_t dir, uint8_t frame, uint8_t action)
|
* Выделено из pop_fore_over_kid, потому что тем же счётом пользуется страж
|
||||||
|
* (pop_fore_over_char): в оригинале это общий код для любого Char.
|
||||||
|
* Результат — в файловых статиках, а не через 6 указателей: на Z80 такой
|
||||||
|
* возврат стоил бы дороже самой функции. */
|
||||||
|
static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw;
|
||||||
|
|
||||||
|
static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
|
int8_t dir, uint8_t sword)
|
||||||
{
|
{
|
||||||
int wh = (int)((w + 1) >> 1); /* char_width_half */
|
int wh = (int)((w + 1) >> 1); /* char_width_half */
|
||||||
int xl = obj_x / 2 + 58; /* char_x_left (58-база) */
|
int xl = obj_x / 2 + 58; /* char_x_left (58-база) */
|
||||||
int xr;
|
int xr;
|
||||||
int8_t cL, cR, cLraw, rT, rB, r, c, rTraw;
|
int8_t cL, cR, cLraw, rT, rB, rTraw;
|
||||||
if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
|
if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
|
||||||
xr = xl + wh;
|
xr = xl + wh;
|
||||||
cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */
|
cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */
|
||||||
cL = cLraw; if (cL < 0) cL = 0; else if (cL > 9) cL = 9;
|
cL = cLraw; if (cL < 0) cL = 0; else if (cL > 9) cL = 9;
|
||||||
cR = col_from_x(xr); if (cR < 0) cR = 0; else if (cR > 9) cR = 9;
|
cR = col_from_x(xr); if (cR < 0) cR = 0; else if (cR > 9) cR = 9;
|
||||||
|
/* redraw_at_char (seg003:0430): «If char is holding sword, it makes
|
||||||
|
* redraw-area bigger» — КЛИНОК торчит на колонку дальше габарита кадра,
|
||||||
|
* и без этого расширения передние грани в той колонке не
|
||||||
|
* перерисовываются: меч остаётся поверх столба, а его след — на фоне. */
|
||||||
|
if (sword >= SWORD_2_DRAWN) {
|
||||||
|
if (dir >= 0) { if (++cR > 9) cR = 9; }
|
||||||
|
else { if (--cL < 0) cL = 0; }
|
||||||
|
}
|
||||||
/* Порт set_char_collision (seg006:0723): нижний ряд = y_to_row_mod4(obj_y),
|
/* Порт set_char_collision (seg006:0723): нижний ряд = y_to_row_mod4(obj_y),
|
||||||
* причём обёртка -1 (Kid НИЖЕ комнаты — climbdown/провал, obj_y в полосе
|
* причём обёртка -1 (Kid НИЖЕ комнаты — climbdown/провал, obj_y в полосе
|
||||||
* «ряд 3») трактуется как ряд 3, НЕ ряд 0. Верхний ряд считается от
|
* «ряд 3») трактуется как ряд 3, НЕ ряд 0. Верхний ряд считается от
|
||||||
@@ -1362,6 +1607,18 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
|||||||
* (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим
|
* (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим
|
||||||
* включение ряда rB-1. */
|
* включение ряда rB-1. */
|
||||||
if (rB > 0 && rT >= rB) rT = rB - 1;
|
if (rB > 0 && rT >= rB) rT = rB - 1;
|
||||||
|
fp_cL = cL; fp_cR = cR; fp_cLraw = cLraw;
|
||||||
|
fp_rT = rT; fp_rB = rB; fp_rTraw = rTraw;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
|
int8_t dir, uint8_t frame, uint8_t action) __banked
|
||||||
|
{
|
||||||
|
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
|
||||||
|
fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||||
|
char_footprint(obj_x, obj_y, w, h, dir, Kid.sword);
|
||||||
|
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
|
||||||
|
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
|
||||||
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
|
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
|
||||||
* Kid попадает в midtable — база порядка окклюзии в other_overlay_tile.
|
* Kid попадает в midtable — база порядка окклюзии в other_overlay_tile.
|
||||||
* run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/
|
* run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/
|
||||||
@@ -1429,4 +1686,37 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
|||||||
g_clip_top = 0;
|
g_clip_top = 0;
|
||||||
}
|
}
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
fclip_on = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Передние грани тайлов поверх ПРОИЗВОЛЬНОГО персонажа (страж, дальше —
|
||||||
|
* тень/скелет). В оригинале любой Char — запись midtable, а foretable
|
||||||
|
* рисуется после всех midtable (draw_tile_fore, seg008:690), поэтому
|
||||||
|
* колонна и кромка пола перекрывают всех одинаково.
|
||||||
|
*
|
||||||
|
* Отличие от pop_fore_over_kid: только слой fore (+ полоса потолка).
|
||||||
|
* Оверлеи тела пола (draw_other_overlay) завязаны на позы Kid — вис,
|
||||||
|
* полёт, подъём, — которых у стража нет: он ходит и дерётся на полу
|
||||||
|
* (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
|
||||||
|
* порт redraw_at_char2, а не догадки сейчас. */
|
||||||
|
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
|
int8_t dir, uint8_t sword) __banked
|
||||||
|
{
|
||||||
|
int8_t r, c;
|
||||||
|
fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||||
|
char_footprint(obj_x, obj_y, w, h, dir, sword);
|
||||||
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
|
for (r = fp_rT; r <= fp_rB; r++)
|
||||||
|
for (c = fp_cL; c <= fp_cR; c++)
|
||||||
|
fore_tile(r, c);
|
||||||
|
if (fp_rTraw < 0) {
|
||||||
|
g_clip_top = POP_YOFF;
|
||||||
|
for (c = fp_cL; c <= fp_cR; c++) {
|
||||||
|
ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H);
|
||||||
|
ceil_over_kid_tile(c);
|
||||||
|
}
|
||||||
|
g_clip_top = 0;
|
||||||
|
}
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
fclip_on = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,18 @@
|
|||||||
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
|
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
|
||||||
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
|
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
|
||||||
|
|
||||||
|
/* Выбор ядра libbgi (pop_onscreen_cols / pop_heal_fast) переехал в
|
||||||
|
* `_pop_draw.h` — его включают только три рисующих модуля. Из общего
|
||||||
|
* заголовка SDCC размножал мёртвые копии этих inline'ов по всем TU: см.
|
||||||
|
* шапку `_pop_draw.h`. */
|
||||||
|
|
||||||
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
|
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
|
||||||
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
|
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
|
||||||
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
|
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
|
||||||
int pop_bg_load(void);
|
int pop_bg_load(void) __banked;
|
||||||
|
|
||||||
/* Освободить EMM-страницы атласов. */
|
/* Освободить EMM-страницы атласов. */
|
||||||
void pop_bg_free(void);
|
void pop_bg_free(void) __banked;
|
||||||
|
|
||||||
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
|
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
|
||||||
* (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца
|
* (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца
|
||||||
@@ -39,97 +44,126 @@ void pop_bg_free(void);
|
|||||||
* тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */
|
* тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */
|
||||||
void pop_room_draw(uint8_t room_num,
|
void pop_room_draw(uint8_t room_num,
|
||||||
const uint8_t *fg, const uint8_t *bg,
|
const uint8_t *fg, const uint8_t *bg,
|
||||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg);
|
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked;
|
||||||
|
|
||||||
/* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright,
|
/* Задать ряд 0 комнаты СНИЗУ — для draw_tile_topright, когда тайл нижнего
|
||||||
* когда тайл нижнего ряда стал empty (после падения loose): «стена вниз»
|
* ряда стал empty (после падения loose): «стена вниз» рисуется по коду
|
||||||
* рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */
|
* room_B(0, col-1). ОДИННАДЦАТЬ байт: [0..9] — колонки комнаты снизу,
|
||||||
void pop_room_set_below(const uint8_t *below_row0_fg);
|
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
|
||||||
|
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
|
||||||
|
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
|
||||||
|
|
||||||
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
||||||
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
|
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
|
||||||
* комнаты сверху (кромка уровня → пол). */
|
* комнаты сверху (кромка уровня → пол). */
|
||||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod);
|
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
|
||||||
|
|
||||||
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
|
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
|
||||||
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
|
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
|
||||||
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
|
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
|
||||||
* берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */
|
* берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */
|
||||||
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
int8_t dir, uint8_t frame, uint8_t action);
|
int8_t dir, uint8_t frame, uint8_t action) __banked;
|
||||||
|
|
||||||
|
/* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с
|
||||||
|
* POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит
|
||||||
|
* тот, кто нарисовал спрайт (kid_draw / pop_guard_draw) — он один знает
|
||||||
|
* итоговый габарит (у Kid — с учётом клинка и обрезки clip_char). Вне
|
||||||
|
* fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и
|
||||||
|
* замер — в pop_bg.c у fclip_on. */
|
||||||
|
void pop_fore_set_clip(int x, int y, int w, int h) __banked;
|
||||||
|
|
||||||
|
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
|
||||||
|
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
|
||||||
|
* (до ×8/7), как у kid_fp_obj_x(). */
|
||||||
|
/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому
|
||||||
|
* вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в
|
||||||
|
* кадр, других вызывающих нет — цена одного трамплина здесь не видна. */
|
||||||
|
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
|
int8_t dir, uint8_t sword) __banked;
|
||||||
|
|
||||||
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
|
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
|
||||||
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
|
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
|
||||||
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
|
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
|
||||||
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
|
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
|
||||||
void pop_fore_heal(void);
|
void pop_fore_heal(void) __banked;
|
||||||
|
|
||||||
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
|
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
|
||||||
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
|
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
|
||||||
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
|
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
|
||||||
* после падения (NORMAL, в ОЗУ-копию). */
|
* после падения (NORMAL, в ОЗУ-копию). */
|
||||||
void pop_loose_shake_draw(int row, int col);
|
void pop_loose_shake_draw(int row, int col) __banked;
|
||||||
void pop_loose_bake_empty(int row, int col);
|
void pop_loose_bake_empty(int row, int col) __banked;
|
||||||
|
|
||||||
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
|
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
|
||||||
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
|
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
|
||||||
* Состояние — pop_ceil_modif[10] в pop_map. */
|
* Состояние — pop_ceil_modif[10] в pop_map. */
|
||||||
void pop_ceil_shake_draw(int col);
|
void pop_ceil_shake_draw(int col) __banked;
|
||||||
void pop_ceil_bake_empty(int col);
|
void pop_ceil_bake_empty(int col) __banked;
|
||||||
|
|
||||||
/* Кусок приземлился (loose_land): 0 = нет, иначе tilepos+1 тайла, на который
|
/* Кусок приземлился (loose_land) — переехал в pop_state.h: переменную
|
||||||
* он лёг. Ставит pop_loose_mob_tick, разбирает pop_loose_tick (debris+knock). */
|
* читают по обе стороны банковой границы. */
|
||||||
extern uint8_t pop_loose_landed;
|
#include "pop_state.h"
|
||||||
|
|
||||||
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
|
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
|
||||||
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
|
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
|
||||||
void pop_spike_redraw(int row, int col);
|
void pop_spike_redraw(int row, int col) __banked;
|
||||||
|
|
||||||
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
|
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
|
||||||
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
|
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
|
||||||
* (пламя рисуется в ячейке правого соседа). */
|
* (пламя рисуется в ячейке правого соседа). */
|
||||||
void pop_torch_draw(int row, int col, uint8_t modif);
|
void pop_torch_draw(int row, int col, uint8_t modif) __banked;
|
||||||
void pop_potion_draw(int row, int col, uint8_t modif);
|
void pop_potion_draw(int row, int col, uint8_t modif) __banked;
|
||||||
|
|
||||||
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
|
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
|
||||||
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
|
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
|
||||||
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
|
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
|
||||||
void pop_room_redraw_seam_left(void);
|
void pop_room_redraw_seam_left(void) __banked;
|
||||||
|
|
||||||
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
|
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
|
||||||
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
|
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
|
||||||
* кнопка нажата/отжата, floor → debris от упавшей плиты. */
|
* кнопка нажата/отжата, floor → debris от упавшей плиты. */
|
||||||
void pop_floor_bake(int row, int col);
|
void pop_floor_bake(int row, int col) __banked;
|
||||||
void pop_button_redraw(int row, int col); /* = pop_floor_bake (кнопка) */
|
void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кнопка) */
|
||||||
|
|
||||||
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
|
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
|
||||||
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
|
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
|
||||||
void pop_leveldoor_redraw(int row, int col);
|
void pop_leveldoor_redraw(int row, int col) __banked;
|
||||||
|
|
||||||
|
/* Ворота ВНУТРИ отрисованной комнаты: перерисовать по живому modif.
|
||||||
|
* (row,col) — координаты САМИХ ворот; бары рисует ячейка правого соседа
|
||||||
|
* (draw_tile, ветка lcode==4), туда и идёт wipe+redraw. Порт draw_trob
|
||||||
|
* (seg007:01E6), которым заканчивается animate_door. */
|
||||||
|
void pop_gate_redraw(int row, int col) __banked;
|
||||||
|
|
||||||
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
|
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
|
||||||
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
|
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
|
||||||
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
|
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
|
||||||
void pop_loose_mob_spawn(int row, int col);
|
void pop_loose_mob_spawn(int row, int col) __banked;
|
||||||
void pop_loose_mob_tick(void);
|
void pop_loose_mob_tick(void) __banked;
|
||||||
void pop_loose_mob_reset(void); /* остановить кусок (смена комнаты) */
|
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
|
||||||
|
|
||||||
|
/* Смена комнаты: куски продолжают падать в СВОИХ комнатах (do_mobs крутит
|
||||||
|
* mobs[] независимо от drawn_room), сбрасывается только heal-история. */
|
||||||
|
void pop_loose_mob_room_changed(void) __banked;
|
||||||
|
|
||||||
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
|
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
|
||||||
* seg008:1561). Звать после kid_draw, перед fore-слоем. */
|
* seg008:1561). Звать после kid_draw, перед fore-слоем. */
|
||||||
void pop_loose_mob_draw_over(void);
|
void pop_loose_mob_draw_over(void) __banked;
|
||||||
|
|
||||||
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
|
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
|
||||||
uint8_t pop_loose_mob_pos(int *col, int *y);
|
uint8_t pop_loose_mob_pos(int *col, int *y) __banked;
|
||||||
|
|
||||||
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
|
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
|
||||||
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
|
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
|
||||||
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
|
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
|
||||||
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
|
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
|
||||||
void pop_clip_sprite(int x, int w, int sy_top, int h);
|
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked;
|
||||||
|
|
||||||
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
|
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
|
||||||
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
|
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
|
||||||
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
|
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
|
||||||
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
|
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
|
||||||
void pop_room_clip_borders(void);
|
void pop_room_clip_borders(void) __banked;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* pop_char.h — состояние ПЕРСОНАЖА, общее для Кида, стража и активной
|
||||||
|
* копии Char. Порт char_type (types.h:302).
|
||||||
|
*
|
||||||
|
* Зачем отдельным заголовком: интерпретатор последовательностей в
|
||||||
|
* оригинале один на всех (работает с Char, а вокруг стоят
|
||||||
|
* loadkid/savekid и loadshad/saveshad, seg006:809). Пока у Кида был свой
|
||||||
|
* усечённый тип, а у стража свой, общий интерпретатор был невозможен.
|
||||||
|
* Поля и их порядок — как в char_type, чтобы порт seg005/seg006 ложился
|
||||||
|
* один в один.
|
||||||
|
*/
|
||||||
|
#ifndef POP_CHAR_H
|
||||||
|
#define POP_CHAR_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t frame;
|
||||||
|
uint8_t x, y;
|
||||||
|
int8_t direction; /* -1 влево, 0 вправо */
|
||||||
|
int8_t curr_col, curr_row;
|
||||||
|
uint8_t action;
|
||||||
|
int8_t fall_x, fall_y;
|
||||||
|
uint8_t room;
|
||||||
|
uint8_t repeat;
|
||||||
|
uint8_t charid; /* 0 = Kid, 2 = страж (см. CHARID_* в pop_guard.h) */
|
||||||
|
uint8_t sword;
|
||||||
|
int8_t alive; /* -1 = жив (как в оригинале), 0 = убит */
|
||||||
|
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
|
||||||
|
} pop_char_t;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* pop_cheat.h — режим читов (порт cheats_enabled, seg000:111).
|
||||||
|
*
|
||||||
|
* В оригинале включается ключом командной строки megahit/debug и меняет
|
||||||
|
* набор доступных клавиш. У нас конфигурации/UI пока нет, поэтому это
|
||||||
|
* просто глобальный флаг: main ставит его при старте (на время разработки —
|
||||||
|
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
|
||||||
|
*
|
||||||
|
* Реализовано: K — убить стража (seg000:786), I/S/Shift+L (наши отладочные),
|
||||||
|
* [ / ] — подгонка Кида по X (seg000:1828). Остальные читы оригинала
|
||||||
|
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
|
||||||
|
*/
|
||||||
|
#ifndef POP_CHEAT_H
|
||||||
|
#define POP_CHEAT_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
|
||||||
|
|
||||||
|
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
|
||||||
|
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
|
||||||
|
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
|
||||||
|
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
|
||||||
|
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
|
||||||
|
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
|
||||||
|
|
||||||
|
/* Последний уровень игры (tbl_* в pop_level рассчитаны на 0..15). */
|
||||||
|
#define POP_LAST_LEVEL 15
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -17,8 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <kbd_raw.h>
|
#include <kbd_raw.h>
|
||||||
|
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
|
||||||
#include "pop_kid.h"
|
#include "pop_kid.h"
|
||||||
#include "pop_map.h"
|
#include "pop_map.h"
|
||||||
|
#include "pop_guard.h" /* charid, состояние меча, seq стража */
|
||||||
|
|
||||||
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
|
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
|
||||||
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
|
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
|
||||||
@@ -51,6 +53,7 @@
|
|||||||
#define SEQ_49_STAND_UP_FROM_CROUCH 49
|
#define SEQ_49_STAND_UP_FROM_CROUCH 49
|
||||||
#define SEQ_50_CROUCH 50
|
#define SEQ_50_CROUCH 50
|
||||||
#define SEQ_79_CROUCH_HOP 79
|
#define SEQ_79_CROUCH_HOP 79
|
||||||
|
#define SEQ_78_DRINK 78 /* выпить зелье */
|
||||||
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
|
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
|
||||||
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
|
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
|
||||||
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
|
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
|
||||||
@@ -58,6 +61,7 @@
|
|||||||
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
|
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
|
||||||
#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */
|
#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */
|
||||||
#define SEQ_25_HANG_AGAINST_WALL 25 /* вис у стены (Shift в висе) */
|
#define SEQ_25_HANG_AGAINST_WALL 25 /* вис у стены (Shift в висе) */
|
||||||
|
#define SEQ_GO_UP_LEVELDOOR 70 /* seq_70_go_up_on_level_door */
|
||||||
|
|
||||||
#define FRAME_7_RUN 7
|
#define FRAME_7_RUN 7
|
||||||
#define FRAME_11_RUN 11
|
#define FRAME_11_RUN 11
|
||||||
@@ -70,18 +74,25 @@
|
|||||||
#define FRAME_109_CROUCH 109
|
#define FRAME_109_CROUCH 109
|
||||||
|
|
||||||
/* ---- Состояние управления (seg006 globals) ------------------------ *
|
/* ---- Состояние управления (seg006 globals) ------------------------ *
|
||||||
* control_x/y/shift — «сырое» состояние осей ЭТОГО кадра (абсолютно:
|
* Сами control_* ЖИВУТ В pop_state.c, а не здесь. Модуль уехал в БАНК 5,
|
||||||
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
|
* а шину пишут с трёх сторон (этот банк, guards.c из банка 1) и читают из
|
||||||
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между
|
* банка 3 (pop_map) — это ровно тот случай, для которого pop_state и
|
||||||
* кадрами (rest/save). */
|
* заведён. Сегодня оно работало бы и отсюда: банки собираются БЕЗ
|
||||||
static int8_t control_x, control_y, control_shift;
|
* --bank-data, поэтому их писучие данные остаются в общем _DATA. Но это
|
||||||
static int8_t control_forward, control_backward, control_up, control_down, control_shift2;
|
* флаг сборки, а не свойство кода: включат --bank-data ради экономии
|
||||||
|
* W1/W2 — и шина молча уедет в страницу банка.
|
||||||
|
*
|
||||||
|
* ctrl1_* остаются здесь: их трогает только этот модуль. */
|
||||||
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
|
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
|
||||||
|
|
||||||
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
|
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
|
||||||
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
|
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
|
||||||
* не дублируются в этот TU). */
|
* не дублируются в этот TU). */
|
||||||
#define seqtbl_offset_char(seq) kid_set_seq(seq)
|
/* В оригинале seqtbl_offset_char работает с АКТИВНЫМ персонажем, и
|
||||||
|
* control() вызывается внутри окна Char. Раньше макрос вёл на
|
||||||
|
* kid_set_seq (пишет прямо в Kid) — после введения окна это ломало ввод:
|
||||||
|
* savekid затирал Kid копией Char со старой curr_seq. */
|
||||||
|
#define seqtbl_offset_char(seq) pop_char_set_seq(seq)
|
||||||
|
|
||||||
static int8_t release_arrows(void)
|
static int8_t release_arrows(void)
|
||||||
{
|
{
|
||||||
@@ -118,20 +129,30 @@ static uint8_t get_item_action(void)
|
|||||||
seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
|
seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
if (a == 3) { /* зелье — выпить (get_item) */
|
||||||
|
control_shift2 = CONTROL_IGNORE;
|
||||||
|
seqtbl_offset_char(SEQ_78_DRINK);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* run_jump (seg005:0AA8). Выравнивание по кромке пола живёт в pop_map
|
||||||
|
* (там тайловые запросы) — pop_run_jump_align; здесь только диспетчерская
|
||||||
|
* часть. Отказ выравнивателя = прыжка в этом кадре НЕТ, и control_up
|
||||||
|
* гасить нельзя: Кид бежит дальше с зажатой «вверх» и попробует снова на
|
||||||
|
* следующем кадре. Ровно так игрок и «ловит» фазу перед провалом. */
|
||||||
static void run_jump(void)
|
static void run_jump(void)
|
||||||
{
|
{
|
||||||
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
|
if (Char.frame >= FRAME_7_RUN) {
|
||||||
* полировка K3; K2b просто запускает run-jump. */
|
if (!pop_run_jump_align()) return;
|
||||||
if (Kid.frame >= FRAME_7_RUN) {
|
|
||||||
control_up = release_arrows();
|
control_up = release_arrows();
|
||||||
seqtbl_offset_char(SEQ_4_RUN_JUMP);
|
seqtbl_offset_char(SEQ_4_RUN_JUMP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void safe_step(void); /* fwd */
|
static void safe_step(void); /* fwd */
|
||||||
|
static void draw_sword(void); /* fwd: нужен в control_standing */
|
||||||
|
|
||||||
static void forward_pressed(void)
|
static void forward_pressed(void)
|
||||||
{
|
{
|
||||||
@@ -172,8 +193,15 @@ static void jump_up(void)
|
|||||||
|
|
||||||
static void up_pressed(void)
|
static void up_pressed(void)
|
||||||
{
|
{
|
||||||
/* (дверь уровня/телепорт — нет в тесте.) ↑+forward = прыжок с места;
|
/* Дверь уровня рядом и открыта → войти в неё: постановку в проём
|
||||||
* иначе вертикальный прыжок вверх. */
|
* (go_up_leveldoor) делает pop_leveldoor_enter, здесь остаётся только
|
||||||
|
* последовательность подъёма по лестнице. Телепортов у нас нет — это
|
||||||
|
* модификация SDLPoP, а не оригинал.
|
||||||
|
* Иначе: ↑+forward = прыжок с места, иначе вертикальный прыжок вверх. */
|
||||||
|
if (pop_leveldoor_enter()) {
|
||||||
|
seqtbl_offset_char(SEQ_GO_UP_LEVELDOOR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (control_x == CONTROL_HELD_FORWARD)
|
if (control_x == CONTROL_HELD_FORWARD)
|
||||||
standing_jump();
|
standing_jump();
|
||||||
else
|
else
|
||||||
@@ -190,15 +218,15 @@ static void safe_step(void)
|
|||||||
control_forward = CONTROL_IGNORE;
|
control_forward = CONTROL_IGNORE;
|
||||||
distance = pop_edge_distance();
|
distance = pop_edge_distance();
|
||||||
if (distance) {
|
if (distance) {
|
||||||
Kid.repeat = 1;
|
Char.repeat = 1;
|
||||||
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
|
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
|
||||||
} else if (pop_edge_type() == EDGE_WALL) {
|
} else if (pop_edge_type() == EDGE_WALL) {
|
||||||
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
|
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
|
||||||
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
|
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
|
||||||
Kid.repeat = 1;
|
Char.repeat = 1;
|
||||||
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
|
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
|
||||||
} else if (Kid.repeat != 0) {
|
} else if (Char.repeat != 0) {
|
||||||
Kid.repeat = 0;
|
Char.repeat = 0;
|
||||||
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
|
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
|
||||||
} else {
|
} else {
|
||||||
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
|
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
|
||||||
@@ -225,7 +253,32 @@ static void control_standing(void)
|
|||||||
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
|
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
|
||||||
get_item_action())
|
get_item_action())
|
||||||
return;
|
return;
|
||||||
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */
|
/* Ветку «charid != kid && вниз+вперёд -> draw_sword» не портируем: у нас
|
||||||
|
* соперник до control_standing не доходит, его перехватывает
|
||||||
|
* control_guard_inactive в самом диспетчере (порядок как в seg005). */
|
||||||
|
|
||||||
|
/* seg005:352 — Кид с мечом ВИДИТ активного стража: достаёт клинок сам.
|
||||||
|
* Без этого игрок стоит с мечом в ножнах, пока его рубят. */
|
||||||
|
if (pop_have_sword) {
|
||||||
|
if (offguard != 0 && control_shift >= CONTROL_RELEASED) {
|
||||||
|
/* Меч убран намеренно (down в бою) — не доставать, пока не
|
||||||
|
* отпустят Shift; в оригинале это переход на loc_6213. */
|
||||||
|
} else if (can_guard_see_kid >= 2) {
|
||||||
|
int16_t d = pop_char_opp_dist();
|
||||||
|
if (d >= -10 && d < 90) {
|
||||||
|
holding_sword = 1;
|
||||||
|
/* (word)d < (word)-6 — беззнаковое: истинно для ВСЕГО, кроме
|
||||||
|
* −6..−1. То есть «страж вплотную за спиной» -> отступить,
|
||||||
|
* иначе достать меч. */
|
||||||
|
if (d >= -6 && d < 0) { back_pressed(); return; }
|
||||||
|
draw_sword();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offguard = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (control_shift == CONTROL_HELD) {
|
if (control_shift == CONTROL_HELD) {
|
||||||
if (control_backward == CONTROL_HELD) back_pressed();
|
if (control_backward == CONTROL_HELD) back_pressed();
|
||||||
else if (control_up == CONTROL_HELD) up_pressed();
|
else if (control_up == CONTROL_HELD) up_pressed();
|
||||||
@@ -266,7 +319,7 @@ static void control_running(void)
|
|||||||
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
|
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
|
||||||
* seq_47 при беге в стену (как оригинал), с корректной позицией. */
|
* seq_47 при беге в стену (как оригинал), с корректной позицией. */
|
||||||
if (control_x == CONTROL_RELEASED &&
|
if (control_x == CONTROL_RELEASED &&
|
||||||
(Kid.frame == FRAME_7_RUN || Kid.frame == FRAME_11_RUN)) {
|
(Char.frame == FRAME_7_RUN || Char.frame == FRAME_11_RUN)) {
|
||||||
control_forward = release_arrows();
|
control_forward = release_arrows();
|
||||||
seqtbl_offset_char(SEQ_13_STOP_RUN);
|
seqtbl_offset_char(SEQ_13_STOP_RUN);
|
||||||
} else if (control_x == CONTROL_HELD_BACKWARD) {
|
} else if (control_x == CONTROL_HELD_BACKWARD) {
|
||||||
@@ -317,21 +370,216 @@ static void control_jumpup(void)
|
|||||||
standing_jump();
|
standing_jump();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */
|
/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
|
||||||
static void control(void)
|
* клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
|
||||||
|
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
|
||||||
|
* уже как выпад. */
|
||||||
|
static void draw_sword(void)
|
||||||
{
|
{
|
||||||
uint8_t f = Kid.frame;
|
uint8_t seq_id = SEQ_55_DRAW_SWORD;
|
||||||
|
control_forward = control_shift2 = release_arrows();
|
||||||
|
if (Char.charid == CHARID_0_KID) {
|
||||||
|
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
|
||||||
|
} else if (Char.charid != CHARID_1_SHADOW) {
|
||||||
|
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
|
||||||
|
}
|
||||||
|
Char.sword = SWORD_2_DRAWN;
|
||||||
|
seqtbl_offset_char(seq_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
|
||||||
|
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч,
|
||||||
|
* вниз без «вперёд» — развернуться (stand flipped). */
|
||||||
|
static void control_guard_inactive(void)
|
||||||
|
{
|
||||||
|
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
|
||||||
|
if (control_forward == CONTROL_HELD) {
|
||||||
|
draw_sword();
|
||||||
|
} else {
|
||||||
|
control_down = CONTROL_IGNORE;
|
||||||
|
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Боёвка (seg005:964..) ---------------------------------------- *
|
||||||
|
* Всё это ветки ОБЩЕГО диспетчера: у стража тот же control(), просто
|
||||||
|
* control_* ему выставляет ИИ (autocontrol_*, guards.c), а не клавиатура. */
|
||||||
|
|
||||||
|
/* back_with_sword (seg005): шаг назад — только из стоек. */
|
||||||
|
static void back_with_sword(void)
|
||||||
|
{
|
||||||
|
uint8_t f = Char.frame;
|
||||||
|
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||||
|
f == FRAME_171_STAND_WITH_SWORD) {
|
||||||
|
control_backward = CONTROL_IGNORE; /* без авто-повтора */
|
||||||
|
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* forward_with_sword (seg005): шаг вперёд; у стража своя последовательность. */
|
||||||
|
static void forward_with_sword(void)
|
||||||
|
{
|
||||||
|
uint8_t f = Char.frame;
|
||||||
|
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||||
|
f == FRAME_171_STAND_WITH_SWORD) {
|
||||||
|
control_forward = CONTROL_IGNORE;
|
||||||
|
seqtbl_offset_char(Char.charid != CHARID_0_KID ? SEQ_56_GUARD_FORWARD
|
||||||
|
: SEQ_86_FORWARD_WITH_SWORD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sword_strike (seg005): выпад из стойки/шага либо добивание после парирования. */
|
||||||
|
static void sword_strike(void)
|
||||||
|
{
|
||||||
|
uint8_t f = Char.frame;
|
||||||
|
uint8_t seq_id;
|
||||||
|
if (f == FRAME_157_WALK_WITH_SWORD || f == FRAME_158_STAND_WITH_SWORD ||
|
||||||
|
f == FRAME_170_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD ||
|
||||||
|
f == FRAME_165_WALK_WITH_SWORD) {
|
||||||
|
seq_id = (Char.charid == CHARID_0_KID) ? SEQ_75_STRIKE : SEQ_58_GUARD_STRIKE;
|
||||||
|
} else if (f == FRAME_150_PARRY || f == FRAME_161_PARRY) {
|
||||||
|
seq_id = SEQ_66_STRIKE_AFTER_PARRY;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
control_shift2 = CONTROL_IGNORE;
|
||||||
|
seqtbl_offset_char(seq_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* parry (seg005): блок. Условие сильно разное у Кида и у соперника —
|
||||||
|
* соперник блокирует ТОЛЬКО на кадре 152 (второй кадр удара), а Кид ещё и
|
||||||
|
* по 153 (и тогда последовательность прокручивается сразу). */
|
||||||
|
static void parry(void)
|
||||||
|
{
|
||||||
|
uint8_t cf = Char.frame, of = Opp.frame;
|
||||||
|
uint8_t seq_id = SEQ_62_PARRY;
|
||||||
|
uint8_t do_play_seq = 0;
|
||||||
|
|
||||||
|
if (cf == FRAME_158_STAND_WITH_SWORD || cf == FRAME_170_STAND_WITH_SWORD ||
|
||||||
|
cf == FRAME_171_STAND_WITH_SWORD || cf == FRAME_168_BACK ||
|
||||||
|
cf == FRAME_165_WALK_WITH_SWORD) {
|
||||||
|
if (pop_char_opp_dist() >= 32 && Char.charid != CHARID_0_KID) {
|
||||||
|
back_with_sword();
|
||||||
|
return;
|
||||||
|
} else if (Char.charid == CHARID_0_KID) {
|
||||||
|
if (of == FRAME_168_BACK) return;
|
||||||
|
if (of != FRAME_151_STRIKE_1 && of != FRAME_152_STRIKE_2 &&
|
||||||
|
of != FRAME_162_BLOCK_TO_STRIKE) {
|
||||||
|
if (of == FRAME_153_STRIKE_3) do_play_seq = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (of != FRAME_152_STRIKE_2) return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (cf != FRAME_167_BLOCKED) return;
|
||||||
|
seq_id = SEQ_61_PARRY_AFTER_STRIKE;
|
||||||
|
}
|
||||||
|
control_up = CONTROL_IGNORE;
|
||||||
|
seqtbl_offset_char(seq_id);
|
||||||
|
if (do_play_seq) play_seq();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* swordfight (seg005:0CDB) — разбор ввода в бою. Порядок веток важен:
|
||||||
|
* удар старше «убрать меч», тот старше блока, блок старше шагов. */
|
||||||
|
static void swordfight(void)
|
||||||
|
{
|
||||||
|
uint8_t f = Char.frame;
|
||||||
|
uint8_t seq_id;
|
||||||
|
|
||||||
|
if (f == FRAME_161_PARRY && control_shift2 >= CONTROL_RELEASED) {
|
||||||
|
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD); /* отскок после парирования */
|
||||||
|
return;
|
||||||
|
} else if (control_shift2 == CONTROL_HELD) {
|
||||||
|
if (Char.charid == CHARID_0_KID) kid_sword_strike = 15;
|
||||||
|
sword_strike();
|
||||||
|
if (control_shift2 == CONTROL_IGNORE) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (control_down == CONTROL_HELD) {
|
||||||
|
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||||
|
f == FRAME_171_STAND_WITH_SWORD) {
|
||||||
|
control_down = CONTROL_IGNORE;
|
||||||
|
Char.sword = SWORD_0_SHEATHED;
|
||||||
|
if (Char.charid == CHARID_0_KID) {
|
||||||
|
offguard = 1; /* убрал намеренно — сразу не доставать */
|
||||||
|
guard_refrac = 9; /* и страж на это время придержан */
|
||||||
|
holding_sword = 0;
|
||||||
|
seq_id = SEQ_93_PUT_SWORD_AWAY_FAST;
|
||||||
|
} else if (Char.charid == CHARID_1_SHADOW) {
|
||||||
|
seq_id = SEQ_92_PUT_SWORD_AWAY;
|
||||||
|
} else {
|
||||||
|
seq_id = SEQ_87_GUARD_BECOME_INACT; /* Кид ушёл из виду */
|
||||||
|
}
|
||||||
|
seqtbl_offset_char(seq_id);
|
||||||
|
}
|
||||||
|
} else if (control_up == CONTROL_HELD) {
|
||||||
|
parry();
|
||||||
|
} else if (control_forward == CONTROL_HELD) {
|
||||||
|
forward_with_sword();
|
||||||
|
} else if (control_backward == CONTROL_HELD) {
|
||||||
|
back_with_sword();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* control_with_sword (seg005:964): пока соперник виден и близко — бой;
|
||||||
|
* иначе страж убирает меч, а Кид просто стоит. */
|
||||||
|
static void control_with_sword(void)
|
||||||
|
{
|
||||||
|
if (Char.action >= 2) return; /* actions_2_hang_climb и выше */
|
||||||
|
if (pop_tile_at(Char.curr_col, Char.curr_row) == 11 /* loose */ ||
|
||||||
|
can_guard_see_kid >= 2) {
|
||||||
|
int16_t d = pop_char_opp_dist();
|
||||||
|
if (d < 90) {
|
||||||
|
if (d >= 0) { swordfight(); return; }
|
||||||
|
/* Соперник за спиной: далеко — развернуться, вплотную — драться. */
|
||||||
|
if (d < -4) { seqtbl_offset_char(SEQ_60_TURN_WITH_SWORD); return; }
|
||||||
|
swordfight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Не видит / далеко. */
|
||||||
|
if (Char.charid < CHARID_2_GUARD) {
|
||||||
|
if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
|
||||||
|
Char.sword = SWORD_0_SHEATHED;
|
||||||
|
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); /* страж убит */
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
swordfight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
|
||||||
|
void pop_control(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t f = Char.frame;
|
||||||
|
/* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0).
|
||||||
|
* Из любой стойки он валится в seq_71_dying; остальное управление в
|
||||||
|
* этом состоянии не обрабатывается. Через это же место проходит чит K
|
||||||
|
* (pop_guard_kill ставит Guard.alive = 0). */
|
||||||
|
if (Char.alive >= 0) {
|
||||||
|
if (f == FRAME_15_STAND || f == FRAME_166_STAND_INACTIVE ||
|
||||||
|
f == FRAME_158_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD)
|
||||||
|
seqtbl_offset_char(SEQ_71_DYING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
|
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
|
||||||
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
|
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
|
||||||
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
|
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
|
||||||
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
|
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
|
||||||
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
|
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
|
||||||
* пару пикселей меньше, чем должен»). */
|
* пару пикселей меньше, чем должен»). */
|
||||||
if (Kid.action == 5 || Kid.action == 4) { /* bumped / in_freefall */
|
if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
|
||||||
release_arrows();
|
release_arrows();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
|
/* Порядок веток — как в seg005: меч старше charid, charid старше кадров.
|
||||||
|
* Пока Kid безоружен, обе ветки — про стража. */
|
||||||
|
if (Char.sword == SWORD_2_DRAWN)
|
||||||
|
control_with_sword();
|
||||||
|
else if (Char.charid >= CHARID_2_GUARD)
|
||||||
|
control_guard_inactive();
|
||||||
|
else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
|
||||||
control_standing();
|
control_standing();
|
||||||
else if (f == FRAME_48_TURN)
|
else if (f == FRAME_48_TURN)
|
||||||
control_turning();
|
control_turning();
|
||||||
@@ -406,12 +654,12 @@ static void flip_control_x(void)
|
|||||||
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
|
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
|
||||||
static void user_control(void)
|
static void user_control(void)
|
||||||
{
|
{
|
||||||
if (Kid.direction >= DIR_RIGHT) {
|
if (Char.direction >= DIR_RIGHT) {
|
||||||
flip_control_x();
|
flip_control_x();
|
||||||
control();
|
pop_control();
|
||||||
flip_control_x();
|
flip_control_x();
|
||||||
} else {
|
} else {
|
||||||
control();
|
pop_control();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +675,7 @@ static void save_ctrl_1(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Публичный API ------------------------------------------------ */
|
/* ---- Публичный API ------------------------------------------------ */
|
||||||
int pop_ctrl_init(void)
|
int pop_ctrl_init(void) __banked
|
||||||
{
|
{
|
||||||
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
|
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
|
||||||
control_shift2 = CONTROL_RELEASED;
|
control_shift2 = CONTROL_RELEASED;
|
||||||
@@ -435,22 +683,46 @@ int pop_ctrl_init(void)
|
|||||||
return kbd_raw_open();
|
return kbd_raw_open();
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_ctrl_close(void)
|
void pop_ctrl_close(void) __banked
|
||||||
{
|
{
|
||||||
kbd_raw_close();
|
kbd_raw_close();
|
||||||
}
|
}
|
||||||
|
|
||||||
int8_t pop_ctrl_shift_held(void)
|
int8_t pop_ctrl_shift_held(void) __banked
|
||||||
{
|
{
|
||||||
return (int8_t)(control_shift == CONTROL_HELD);
|
return (int8_t)(control_shift == CONTROL_HELD);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_ctrl_tick(void)
|
void pop_ctrl_tick(void) __banked
|
||||||
{
|
{
|
||||||
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */
|
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */
|
||||||
|
/* control_kid (seg006:0CD1), ПЕРВАЯ строка: HP кончились — персонаж
|
||||||
|
* мёртв. Без этой ветки смерть от меча стража не доводилась до конца:
|
||||||
|
* hurt_by_sword играл seq_85, hitp_curr падал в 0, но Kid.alive
|
||||||
|
* оставался −1 и pop_kid_dead не взводился — Кид досматривал сиквенс,
|
||||||
|
* возвращался в стойку с нулём HP, и страж убивал его снова по кругу
|
||||||
|
* (BUG-DEATH-1). pop_kid_dead — наш сигнал главному циклу на респавн
|
||||||
|
* (в оригинале это счётчик Char.alive в play_kid, seg006:0BEE). */
|
||||||
|
if (Kid.alive < 0 && hitp_curr == 0) {
|
||||||
|
Kid.alive = 0;
|
||||||
|
pop_kid_dead = 1;
|
||||||
|
}
|
||||||
rest_ctrl_1();
|
rest_ctrl_1();
|
||||||
read_input();
|
read_input();
|
||||||
read_user_control();
|
read_user_control();
|
||||||
user_control(); /* -> control(): может сменить Kid.curr_seq */
|
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
|
||||||
|
* персонажем (как в оригинале), поэтому ввод игрока применяется к
|
||||||
|
* Киду только внутри loadkid/savekid.
|
||||||
|
*
|
||||||
|
* load_fram_det_col ОБЯЗАТЕЛЕН здесь — порт play_kid_frame
|
||||||
|
* (seg000:1209), где он стоит ровно между loadkid_and_opp() и
|
||||||
|
* play_kid(). cur_frame один на всех персонажей, и в кадре последним
|
||||||
|
* его перезаписывает страж; без перезагрузки диспетчер считает
|
||||||
|
* геометрию Кида (dx_weight -> determine_col -> distance_to_edge_weight
|
||||||
|
* -> get_edge_distance) по кадру СТРАЖА. Замерено: BUG-CTRL-FRAME-1. */
|
||||||
|
pop_loadkid();
|
||||||
|
pop_load_fram_det_col();
|
||||||
|
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
|
||||||
|
pop_savekid_state();
|
||||||
save_ctrl_1();
|
save_ctrl_1();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,18 +18,51 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* МОДУЛЬ ЖИВЁТ В БАНКЕ 5 (--bank 5=pop_ctrl.c), поэтому всё, что зовут
|
||||||
|
* снаружи, помечено __banked. Зовут с трёх сторон: главный цикл
|
||||||
|
* (резидент), guards.c (банк 1) и pop_map.c (банк 3) — банк→банк
|
||||||
|
* работает, трамплин сохраняет страницу окна на стеке. Диспетчер
|
||||||
|
* дёргается один раз за кадр на персонажа, так что цена перехода
|
||||||
|
* незаметна. Сама шина control_* лежит в pop_state.c (см. ниже). */
|
||||||
|
|
||||||
/* Открыть raw-канал клавиатуры и обнулить память авто-повтора.
|
/* Открыть raw-канал клавиатуры и обнулить память авто-повтора.
|
||||||
* 0 — OK, -1 — ошибка (EBUSY и пр., см. kbd_raw_open). */
|
* 0 — OK, -1 — ошибка (EBUSY и пр., см. kbd_raw_open). */
|
||||||
int pop_ctrl_init(void);
|
int pop_ctrl_init(void) __banked;
|
||||||
void pop_ctrl_close(void);
|
void pop_ctrl_close(void) __banked;
|
||||||
|
|
||||||
/* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать
|
/* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать
|
||||||
* диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД
|
* диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД
|
||||||
* kid_tick() (play_seq продолжит с новой последовательности). */
|
* kid_tick() (play_seq продолжит с новой последовательности). */
|
||||||
void pop_ctrl_tick(void);
|
void pop_ctrl_tick(void) __banked;
|
||||||
|
|
||||||
/* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map):
|
/* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map):
|
||||||
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
|
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
|
||||||
int8_t pop_ctrl_shift_held(void);
|
int8_t pop_ctrl_shift_held(void) __banked;
|
||||||
|
|
||||||
|
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
|
||||||
|
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
|
||||||
|
* диспетчера: он ВЫСТАВЛЯЕТ эти переменные, а дальше исполняется тот же
|
||||||
|
* control(), что и для игрока. Значения — CONTROL_RELEASED/IGNORE/HELD. */
|
||||||
|
#define POP_CONTROL_RELEASED 0
|
||||||
|
#define POP_CONTROL_IGNORE 1
|
||||||
|
#define POP_CONTROL_HELD (-1)
|
||||||
|
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
|
||||||
|
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
|
||||||
|
* заполняет как есть, без flip_control_x. */
|
||||||
|
/* Определены в pop_state.c (резидент W1/W2), а не в pop_ctrl.c: шину
|
||||||
|
* трогают три банка сразу, а данные банкового модуля живут в _DATA лишь
|
||||||
|
* пока сборка идёт без --bank-data. */
|
||||||
|
extern int8_t control_x, control_y, control_shift;
|
||||||
|
#define POP_CONTROL_HELD_FORWARD (-1)
|
||||||
|
#define POP_CONTROL_HELD_BACKWARD 1
|
||||||
|
#define POP_CONTROL_HELD_UP (-1)
|
||||||
|
#define POP_CONTROL_HELD_DOWN 1
|
||||||
|
extern int8_t control_forward, control_backward, control_up, control_down,
|
||||||
|
control_shift2;
|
||||||
|
|
||||||
|
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
|
||||||
|
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из
|
||||||
|
* своего тика напрямую. */
|
||||||
|
void pop_control(void) __banked;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* pop_draw.c — выбор ядра libbgi для heal: линейное «noclip», если
|
||||||
|
* прямоугольник целиком на экране, иначе общее клипающее. Объявление и
|
||||||
|
* мотивация — `_pop_draw.h`.
|
||||||
|
*
|
||||||
|
* ПОЧЕМУ ОТДЕЛЬНЫЙ РЕЗИДЕНТНЫЙ МОДУЛЬ, а не inline в заголовке. Замер
|
||||||
|
* CLIP-1 (2026-08-01), SDCC 4.5: `static inline` тут обходится втридорога —
|
||||||
|
* компилятор И встраивает тело в каждый вызов (181 Б на место), И оставляет
|
||||||
|
* отдельную мёртвую копию в каждом TU. У стража это раздуло
|
||||||
|
* `pop_guard_heal` с ~60 до 663 Б. Пяти местам вызова это стоило бы ~1.4 КБ
|
||||||
|
* ради экономии ~50 тактов на вызов — при том что BANK2 занят на 86 %.
|
||||||
|
*
|
||||||
|
* Резидент W1 (_CODE), а не банк: сюда зовут из ДВУХ разных банков
|
||||||
|
* (pop_bg.c → BANK2, pop_gdraw.c → BANK4) и из резидента (pop_kid.c). W1
|
||||||
|
* замаплено всегда, поэтому из банка это обычный `call` без трамплина —
|
||||||
|
* тот же приём, что у pop_sword_draw.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include "_pop_draw.h"
|
||||||
|
|
||||||
|
void pop_heal_fast(int x, int y, int w, int h)
|
||||||
|
{
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
if (pop_onscreen_cols(x, y, (uint16_t)w, (uint16_t)h))
|
||||||
|
gfx_heal_noclip(x, y, (uint8_t)w, (uint8_t)h);
|
||||||
|
else
|
||||||
|
gfx_heal(x, y, w, h);
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
* pop_gdraw.c — ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c).
|
||||||
|
*
|
||||||
|
* Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ
|
||||||
|
* целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны
|
||||||
|
* остаться в W1/W2 — из банка (guards.c) резидент W3 недостижим ни прямо,
|
||||||
|
* ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит
|
||||||
|
* ровно между «что рисует» (зовёт только главный цикл → резидент) и «что
|
||||||
|
* знает» (зовёт и банк → W1/W2).
|
||||||
|
*
|
||||||
|
* Писучие статики резидента (--dataseg не трогаем) остаются в обычном
|
||||||
|
* _DATA, то есть в W2 — сюда переезжает КОД, не данные.
|
||||||
|
*/
|
||||||
|
#include <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include "pop_guard.h"
|
||||||
|
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
|
||||||
|
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
|
||||||
|
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
|
||||||
|
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
|
||||||
|
#include "kid_data.h"
|
||||||
|
|
||||||
|
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
|
||||||
|
#define GUARD_PAGES 5
|
||||||
|
static atlas_t gp[GUARD_PAGES];
|
||||||
|
static uint8_t g_ok;
|
||||||
|
|
||||||
|
/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid:
|
||||||
|
* heal обязан стирать спрайт именно той страницы, в которую рисуем). */
|
||||||
|
static int gx_l[2], gy_l[2];
|
||||||
|
static uint16_t gw_l[2], gh_l[2];
|
||||||
|
static uint8_t gvalid[2];
|
||||||
|
|
||||||
|
/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение
|
||||||
|
* почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит
|
||||||
|
* ровно по площади. Объединение считаем только для окна fore-клипа. */
|
||||||
|
static int sx_l[2], sy_l[2];
|
||||||
|
static uint16_t sw_l[2], sh_l[2];
|
||||||
|
static uint8_t svalid[2];
|
||||||
|
|
||||||
|
/* Ширина спрайта последнего кадра стража — нужна set_char_collision
|
||||||
|
* (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам:
|
||||||
|
* это свойство кадра, а не буфера. Отставание на кадр допустимо — так же
|
||||||
|
* работает kid_fp_width() у Кида. */
|
||||||
|
static uint16_t g_last_w;
|
||||||
|
|
||||||
|
uint16_t pop_guard_fp_width(void) __banked { return g_last_w; }
|
||||||
|
|
||||||
|
int pop_guard_load(void) __banked
|
||||||
|
{
|
||||||
|
static const char *const names[GUARD_PAGES] = {
|
||||||
|
"GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
|
||||||
|
"GUARD\\g3.atl", "GUARD\\g4.atl"
|
||||||
|
};
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < GUARD_PAGES; i++)
|
||||||
|
if (atlas_load(&gp[i], names[i]) != 0) return -1;
|
||||||
|
g_ok = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_guard_free(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
if (!g_ok) return;
|
||||||
|
for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]);
|
||||||
|
g_ok = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же
|
||||||
|
* причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в
|
||||||
|
* большинстве кадров noclip-ядром. Два прямоугольника, а не объединение
|
||||||
|
* (см. комментарий к sx_l выше). */
|
||||||
|
void pop_guard_heal(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t p = gfx_get_draw_page() & 1;
|
||||||
|
if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
|
||||||
|
if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]);
|
||||||
|
gvalid[p] = svalid[p] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_guard_draw(void) __banked
|
||||||
|
{
|
||||||
|
int obj_x, obj_y, top, fwd, fp_x;
|
||||||
|
uint8_t page, idx, p, flip;
|
||||||
|
const uint8_t *img;
|
||||||
|
uint16_t w, h;
|
||||||
|
/* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам
|
||||||
|
* футпринт (pop_fore_over_char) считается по спрайту стража, поэтому
|
||||||
|
* w/h ниже НЕ трогаем. */
|
||||||
|
int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0;
|
||||||
|
|
||||||
|
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
|
||||||
|
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
|
||||||
|
fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx);
|
||||||
|
obj_x = (fwd << 1) - 116;
|
||||||
|
obj_y = (int)Guard.y + pop_gframe.dy;
|
||||||
|
if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++;
|
||||||
|
fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */
|
||||||
|
obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */
|
||||||
|
|
||||||
|
page = (uint8_t)(pop_gframe.image >> 3);
|
||||||
|
idx = (uint8_t)(pop_gframe.image & 7);
|
||||||
|
if (page >= GUARD_PAGES) return;
|
||||||
|
img = (const uint8_t *)atlas_image(&gp[page], idx);
|
||||||
|
gfx_w0_map(gp[page].page);
|
||||||
|
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||||
|
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||||
|
if (w && h) {
|
||||||
|
int bx = obj_x;
|
||||||
|
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
|
||||||
|
if (flip) bx -= (int)w;
|
||||||
|
top = obj_y - (int)h + 1;
|
||||||
|
p = gfx_get_draw_page() & 1;
|
||||||
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
|
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
|
||||||
|
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||||
|
else
|
||||||
|
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
|
||||||
|
g_last_w = w; /* для char_width_half, см. выше */
|
||||||
|
clip_x = bx; clip_y = top; clip_w = w; clip_h = h;
|
||||||
|
}
|
||||||
|
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
|
||||||
|
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
|
||||||
|
* heal; окно fore-клипа — объединение (без него клинок полез бы поверх
|
||||||
|
* переднего столба). */
|
||||||
|
{
|
||||||
|
int sx, sy; uint16_t sw, sh;
|
||||||
|
if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y,
|
||||||
|
&sx, &sy, &sw, &sh)) {
|
||||||
|
p = gfx_get_draw_page() & 1;
|
||||||
|
sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh;
|
||||||
|
svalid[p] = 1;
|
||||||
|
if (!clip_w) {
|
||||||
|
clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh;
|
||||||
|
} else {
|
||||||
|
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
|
||||||
|
if (sx < clip_x) clip_x = sx;
|
||||||
|
if (sy < clip_y) clip_y = sy;
|
||||||
|
if (sx + (int)sw > x1) x1 = sx + (int)sw;
|
||||||
|
if (sy + (int)sh > y1) y1 = sy + (int)sh;
|
||||||
|
clip_w = (uint16_t)(x1 - clip_x);
|
||||||
|
clip_h = (uint16_t)(y1 - clip_y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gfx_w0_unmap();
|
||||||
|
/* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
|
||||||
|
* fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */
|
||||||
|
pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
|
||||||
|
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
|
||||||
|
* midtable, а foretable рисуется после всех midtable-записей, поэтому
|
||||||
|
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
|
||||||
|
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
|
||||||
|
* столб. */
|
||||||
|
if (w && h)
|
||||||
|
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
|
||||||
|
* Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри
|
||||||
|
* тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража
|
||||||
|
* справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2
|
||||||
|
* id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы
|
||||||
|
* просто не печатаем — полоса и так на чёрном фоне.
|
||||||
|
*
|
||||||
|
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
|
||||||
|
* pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */
|
||||||
|
#define HP_Y 194
|
||||||
|
#define HP_STEP 7
|
||||||
|
#define HP_KID_FULL 216
|
||||||
|
#define HP_KID_EMPTY 217
|
||||||
|
#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */
|
||||||
|
|
||||||
|
/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ
|
||||||
|
* страницах дабл-буфера (иначе на второй останется старое значение и его
|
||||||
|
* будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг.
|
||||||
|
*
|
||||||
|
* Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80
|
||||||
|
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
||||||
|
* sdcc_z80_cmp_store_a_bug). */
|
||||||
|
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
||||||
|
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
||||||
|
|
||||||
|
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
||||||
|
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
||||||
|
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
|
||||||
|
|
||||||
|
void pop_hp_draw(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t i, n;
|
||||||
|
uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0);
|
||||||
|
uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0);
|
||||||
|
|
||||||
|
{ /* Изменилось хоть одно число — обновить обе страницы. */
|
||||||
|
uint8_t k = hitp_curr, km = hitp_max;
|
||||||
|
uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev ||
|
||||||
|
gd != hp_gd_prev || gm != hp_gdmax_prev);
|
||||||
|
hp_kid_prev = k; hp_kidmax_prev = km;
|
||||||
|
hp_gd_prev = gd; hp_gdmax_prev = gm;
|
||||||
|
if (changed) hp_todo = 2;
|
||||||
|
}
|
||||||
|
if (hp_todo == 0) return;
|
||||||
|
hp_todo--;
|
||||||
|
|
||||||
|
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
setfillstyle(SOLID_FILL, BLACK);
|
||||||
|
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||||
|
|
||||||
|
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
|
||||||
|
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
|
||||||
|
for (i = hitp_curr; i < n; i++)
|
||||||
|
pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF);
|
||||||
|
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++)
|
||||||
|
pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF);
|
||||||
|
|
||||||
|
/* Страж: только полные деления, справа налево. Скелет полосы не имеет. */
|
||||||
|
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) {
|
||||||
|
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
|
||||||
|
uint16_t w, h;
|
||||||
|
gfx_w0_map(gp[0].page);
|
||||||
|
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||||
|
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||||
|
if (w && h) {
|
||||||
|
/* Атлас стража тоже column-major — блит колоночный. */
|
||||||
|
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
|
||||||
|
for (i = 0; i < n; i++)
|
||||||
|
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
|
||||||
|
HP_Y + POP_YOFF, img, 0, 0, 0);
|
||||||
|
}
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* pop_geom.c — общая геометрия + PRNG оригинала. См. pop_geom.h.
|
||||||
|
*/
|
||||||
|
#include "pop_geom.h"
|
||||||
|
|
||||||
|
/* data.h: шаг 14 (ширина тайла в координатах персонажа), [5] = col 0. */
|
||||||
|
const int16_t pop_x_bump[20] = {
|
||||||
|
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
|
||||||
|
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
|
||||||
|
};
|
||||||
|
const int16_t pop_y_land[5] = {-8, 55, 118, 181, 244};
|
||||||
|
|
||||||
|
int8_t pop_y_to_row(int16_t y)
|
||||||
|
{
|
||||||
|
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
|
||||||
|
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
|
||||||
|
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
|
||||||
|
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
|
||||||
|
{
|
||||||
|
uint16_t n = (uint16_t)(maxv + 1);
|
||||||
|
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
|
||||||
|
return (uint16_t)(v & (uint16_t)(n - 1));
|
||||||
|
return (uint16_t)(v % n);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if POP_PRANDOM_EXACT
|
||||||
|
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
|
||||||
|
*
|
||||||
|
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
|
||||||
|
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong — 8 430
|
||||||
|
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
|
||||||
|
* половины это ~3 500. Здесь ~1 000, при полностью той же
|
||||||
|
* последовательности, что в SDLPoP. Альтернатива «взять генератор
|
||||||
|
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
|
||||||
|
* совместимость с эталоном, по которому сверяем картинку.
|
||||||
|
*
|
||||||
|
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
|
||||||
|
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
|
||||||
|
* 0x343FD) — 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
|
||||||
|
* нужная в конце, попадается по дороге на втором шаге — её и сохраняем.
|
||||||
|
*
|
||||||
|
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
|
||||||
|
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор —
|
||||||
|
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s — в статиках
|
||||||
|
* (регистров на всё не хватает).
|
||||||
|
*/
|
||||||
|
uint16_t pop_pr_ptr; /* адрес сида */
|
||||||
|
uint16_t pop_pr_sh; /* старшее слово исходного сида */
|
||||||
|
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
|
||||||
|
|
||||||
|
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
|
||||||
|
{
|
||||||
|
(void)seed;
|
||||||
|
__asm
|
||||||
|
ld (_pop_pr_ptr), hl
|
||||||
|
ld c, (hl)
|
||||||
|
inc hl
|
||||||
|
ld b, (hl) ; BC = s.lo
|
||||||
|
inc hl
|
||||||
|
ld a, (hl)
|
||||||
|
ld (_pop_pr_sh), a
|
||||||
|
inc hl
|
||||||
|
ld a, (hl)
|
||||||
|
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
|
||||||
|
|
||||||
|
ld h, b
|
||||||
|
ld l, c
|
||||||
|
ld de, (_pop_pr_sh) ; акк = s
|
||||||
|
|
||||||
|
;; акк = акк*2 + s => 3s
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, bc
|
||||||
|
ex de, hl
|
||||||
|
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
|
||||||
|
adc a, l
|
||||||
|
ld l, a
|
||||||
|
ld a, (_pop_pr_sh + 1)
|
||||||
|
adc a, h
|
||||||
|
ld h, a
|
||||||
|
ex de, hl
|
||||||
|
ld (_pop_pr_3s_lo), hl
|
||||||
|
ld (_pop_pr_3s_hi), de
|
||||||
|
|
||||||
|
;; акк = акк*4 + s => 13s
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, bc
|
||||||
|
ex de, hl
|
||||||
|
ld a, (_pop_pr_sh)
|
||||||
|
adc a, l
|
||||||
|
ld l, a
|
||||||
|
ld a, (_pop_pr_sh + 1)
|
||||||
|
adc a, h
|
||||||
|
ld h, a
|
||||||
|
ex de, hl
|
||||||
|
|
||||||
|
;; акк = акк*16 + s => 209s
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, bc
|
||||||
|
ex de, hl
|
||||||
|
ld a, (_pop_pr_sh)
|
||||||
|
adc a, l
|
||||||
|
ld l, a
|
||||||
|
ld a, (_pop_pr_sh + 1)
|
||||||
|
adc a, h
|
||||||
|
ld h, a
|
||||||
|
ex de, hl
|
||||||
|
|
||||||
|
;; акк *= 1024 => 214016s (10 удвоений)
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
add hl, hl
|
||||||
|
rl e
|
||||||
|
rl d
|
||||||
|
|
||||||
|
;; акк -= 3s => 214013s
|
||||||
|
ld bc, (_pop_pr_3s_lo)
|
||||||
|
or a ; CY = 0
|
||||||
|
sbc hl, bc
|
||||||
|
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
|
||||||
|
ex de, hl
|
||||||
|
sbc hl, bc
|
||||||
|
ex de, hl
|
||||||
|
|
||||||
|
;; акк += 2531011 = 0x0026_9EC3
|
||||||
|
ld bc, #0x9EC3
|
||||||
|
add hl, bc
|
||||||
|
ld bc, #0x0026
|
||||||
|
ex de, hl
|
||||||
|
adc hl, bc
|
||||||
|
ex de, hl
|
||||||
|
|
||||||
|
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
|
||||||
|
push de
|
||||||
|
ex de, hl ; DE = младшее слово
|
||||||
|
ld hl, (_pop_pr_ptr)
|
||||||
|
ld (hl), e
|
||||||
|
inc hl
|
||||||
|
ld (hl), d
|
||||||
|
inc hl
|
||||||
|
pop de ; DE = старшее слово
|
||||||
|
ld (hl), e
|
||||||
|
inc hl
|
||||||
|
ld (hl), d
|
||||||
|
ret
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||||||
|
{
|
||||||
|
return pop_rnd_fit(pop_lcg_step(seed), maxv);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
|
||||||
|
|
||||||
|
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
|
||||||
|
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов — при двух
|
||||||
|
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
|
||||||
|
* умножений нет вовсе, только сдвиги и xor.
|
||||||
|
*
|
||||||
|
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
|
||||||
|
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
|
||||||
|
* младшие биты вырождены — бит 0 просто чередуется. Наши вызовы это
|
||||||
|
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
|
||||||
|
* вместо шума получилась бы правильная шахматка. У xorshift такой
|
||||||
|
* структуры нет, а стоит он примерно столько же.
|
||||||
|
*
|
||||||
|
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
|
||||||
|
* xorshift ноль — неподвижная точка, а сид кладки (номер комнаты + ряд +
|
||||||
|
* колонка) вполне может оказаться нулём.
|
||||||
|
*
|
||||||
|
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP — другая (но
|
||||||
|
* статистически такая же) раскладка кладки и другие броски в боёвке.
|
||||||
|
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
|
||||||
|
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
|
||||||
|
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
|
||||||
|
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||||||
|
{
|
||||||
|
uint16_t s = seed->lo;
|
||||||
|
s ^= (uint16_t)(s << 7);
|
||||||
|
s ^= (uint16_t)(s >> 9);
|
||||||
|
s ^= (uint16_t)(s << 8);
|
||||||
|
s = (uint16_t)(s + 0x9E37u);
|
||||||
|
seed->lo = s;
|
||||||
|
seed->hi = 0; /* старшая половина не используется */
|
||||||
|
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
|
||||||
|
* младших бит, а вызывающие берут от результата именно младшие. */
|
||||||
|
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* pop_geom.h — общая геометрия PoP и PRNG оригинала.
|
||||||
|
*
|
||||||
|
* Сюда сведено то, что раньше дублировалось по модулям (x_bump/y_land в
|
||||||
|
* pop_kid и pop_map, y_to_row в pop_bg и pop_map, 32-битный LCG в pop_bg и
|
||||||
|
* pop_trob). Кроме экономии это ФУНДАМЕНТ bank-safe API: модуль живёт в
|
||||||
|
* W1/W2 и не трогает графику, поэтому его может звать и главный цикл, и
|
||||||
|
* резидент W3, и будущий __banked-код стражей (docs/layout_plan_v2.md §4).
|
||||||
|
*/
|
||||||
|
#ifndef POP_GEOM_H
|
||||||
|
#define POP_GEOM_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define FIRST_ONSCREEN_COLUMN 5 /* индекс col 0 комнаты в x_bump */
|
||||||
|
#define TILE_SIZEX 14 /* ширина тайла в координатах персонажа */
|
||||||
|
#define TILE_SIZEY 63 /* высота ряда */
|
||||||
|
|
||||||
|
/* x_bump (data.h): левая граница колонки в координатах персонажа; колонка
|
||||||
|
* комнаты col = x_bump[col + FIRST_ONSCREEN_COLUMN]. y_land: пол ряда
|
||||||
|
* (индекс row+1; [0] — ряд «над комнатой»). */
|
||||||
|
extern const int16_t pop_x_bump[20];
|
||||||
|
extern const int16_t pop_y_land[5];
|
||||||
|
|
||||||
|
/* y_to_row_mod4 (seg006): ряд по координате y с оборотом mod 4 (−1 = полоса
|
||||||
|
* над комнатой, 3 = под комнатой). */
|
||||||
|
int8_t pop_y_to_row(int16_t y);
|
||||||
|
|
||||||
|
/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
|
||||||
|
* анимаций тайлов (фаза факелов) свои последовательности, смешивать их
|
||||||
|
* нельзя.
|
||||||
|
*
|
||||||
|
* ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c):
|
||||||
|
* 1 (по умолчанию) — LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же
|
||||||
|
* раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке
|
||||||
|
* можно сверяться с эталоном;
|
||||||
|
* 0 — xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на
|
||||||
|
* 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3,
|
||||||
|
* 175 кадров: 403 632 против 397 986). Последовательность другая —
|
||||||
|
* статистически эквивалентная, но не та же.
|
||||||
|
*
|
||||||
|
* Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит
|
||||||
|
* умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в
|
||||||
|
* MAME, breakpoint на __mullong).
|
||||||
|
*
|
||||||
|
* ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией
|
||||||
|
* SDCC — те же генераторы на Z80-ассемблере укладываются в 86..148 тактов.
|
||||||
|
* Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там
|
||||||
|
* умножение на константу), а менять генератор — только если и этого мало. */
|
||||||
|
typedef struct { uint16_t lo, hi; } pop_rnd_t;
|
||||||
|
|
||||||
|
#ifndef POP_PRANDOM_EXACT
|
||||||
|
#define POP_PRANDOM_EXACT 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);
|
||||||
|
|
||||||
|
/* Сид из 16-битного числа (старшая половина = 0). */
|
||||||
|
#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0)
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
* pop_guard.c — СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h.
|
||||||
|
*
|
||||||
|
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
|
||||||
|
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
|
||||||
|
* ОТРИСОВКА — в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл).
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "pop_guard.h"
|
||||||
|
#include "pop_geom.h"
|
||||||
|
#include "pop_level.h"
|
||||||
|
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
|
||||||
|
#include "pop_map.h" /* hitp_* — HP Кида */
|
||||||
|
#include "kid_data.h"
|
||||||
|
|
||||||
|
pop_char_t Guard;
|
||||||
|
uint8_t guardhp_curr, guardhp_max;
|
||||||
|
int8_t guardhp_delta;
|
||||||
|
int8_t can_guard_see_kid;
|
||||||
|
int8_t is_guard_notice;
|
||||||
|
uint8_t guard_skill;
|
||||||
|
int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||||
|
int8_t offguard, holding_sword;
|
||||||
|
|
||||||
|
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
|
||||||
|
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
|
||||||
|
* см. pop_geom.h — иначе перерисовка стены сбивала бы броски стража. */
|
||||||
|
pop_rnd_t pop_fight_seed;
|
||||||
|
|
||||||
|
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
|
||||||
|
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
|
||||||
|
kframe pop_gframe;
|
||||||
|
|
||||||
|
uint8_t pop_guard_present(void)
|
||||||
|
{
|
||||||
|
return (uint8_t)(Guard.charid != 0 && Guard.alive < 0 && guardhp_curr != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_guard_reset(void)
|
||||||
|
{
|
||||||
|
Guard.charid = 0;
|
||||||
|
Guard.alive = 0;
|
||||||
|
Guard.direction = 0;
|
||||||
|
guardhp_curr = 0;
|
||||||
|
guardhp_delta = 0;
|
||||||
|
can_guard_see_kid = 0;
|
||||||
|
is_guard_notice = 0;
|
||||||
|
guard_refrac = justblocked = kid_sword_strike = 0;
|
||||||
|
offguard = holding_sword = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* kill guard cheat (seg000:786):
|
||||||
|
* if (Guard.charid != charid_4_skeleton) {
|
||||||
|
* guardhp_delta = -guardhp_curr;
|
||||||
|
* Guard.alive = 0;
|
||||||
|
* }
|
||||||
|
* Скелета чит не убивает — он и в оригинале бессмертен. */
|
||||||
|
void pop_guard_kill(void)
|
||||||
|
{
|
||||||
|
if (Guard.charid == CHARID_4_SKELETON) return;
|
||||||
|
if (!pop_guard_present()) return;
|
||||||
|
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
|
||||||
|
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
|
||||||
|
* общий control() ставит ему seq_71_dying. */
|
||||||
|
guardhp_delta = (int8_t)-guardhp_curr;
|
||||||
|
Guard.alive = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
|
||||||
|
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
|
||||||
|
void pop_do_delta_hp(void)
|
||||||
|
{
|
||||||
|
int16_t hp;
|
||||||
|
if (hitp_delta) {
|
||||||
|
hp = (int16_t)hitp_curr + hitp_delta;
|
||||||
|
if (hp < 0) hp = 0;
|
||||||
|
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
|
||||||
|
hitp_curr = (uint8_t)hp;
|
||||||
|
pop_kid_hurt = 1; /* -> «брызги» поверх Кида */
|
||||||
|
hitp_delta = 0;
|
||||||
|
}
|
||||||
|
if (guardhp_delta) {
|
||||||
|
hp = (int16_t)guardhp_curr + guardhp_delta;
|
||||||
|
if (hp < 0) hp = 0;
|
||||||
|
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
|
||||||
|
guardhp_curr = (uint8_t)hp;
|
||||||
|
guardhp_delta = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
|
||||||
|
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где
|
||||||
|
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||||||
|
void pop_guard_load_frame(void)
|
||||||
|
{
|
||||||
|
int16_t idx = (int16_t)Guard.frame;
|
||||||
|
if (idx >= 102 && idx < 107) idx += 70; /* add_frame */
|
||||||
|
idx -= 149;
|
||||||
|
if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; }
|
||||||
|
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
|
||||||
|
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
|
||||||
|
* последовательностью — тогда при возврате он лежит там же, а не встаёт
|
||||||
|
* живым. */
|
||||||
|
void pop_guard_leave(void)
|
||||||
|
{
|
||||||
|
if (Guard.charid == 0) return; /* стража не было */
|
||||||
|
pop_guard_state_save(Guard.room,
|
||||||
|
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
|
||||||
|
Guard.direction, Guard.x, guard_skill,
|
||||||
|
Guard.curr_seq,
|
||||||
|
(uint8_t)(Guard.alive >= 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа —
|
||||||
|
* floor((xpos − 7 − 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */
|
||||||
|
static int8_t guard_col_from_x(uint8_t xpos)
|
||||||
|
{
|
||||||
|
int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN];
|
||||||
|
int16_t col = x / TILE_SIZEX;
|
||||||
|
if (x % TILE_SIZEX < 0) --col;
|
||||||
|
return (int8_t)col;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_guard_enter(uint8_t room)
|
||||||
|
{
|
||||||
|
uint8_t tile, color, skill;
|
||||||
|
int8_t dir;
|
||||||
|
pop_guard_reset();
|
||||||
|
/* load_lev_spr (seg000:1112): tbl_guard_type == −1 — стражей на уровне
|
||||||
|
* НЕТ вообще (14/15), их графика даже не грузится. Без этой проверки
|
||||||
|
* на таком уровне полезли бы стражи из данных комнат. */
|
||||||
|
if (pop_level_guard_type(pop_current_level) < 0) return;
|
||||||
|
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
|
||||||
|
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
|
||||||
|
* колонки тайла, а не берётся из файла. */
|
||||||
|
Guard.room = room;
|
||||||
|
Guard.curr_row = (int8_t)(tile / 10);
|
||||||
|
Guard.y = (uint8_t)pop_y_land[Guard.curr_row + 1];
|
||||||
|
Guard.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||||||
|
Guard.curr_col = (int8_t)(tile % 10);
|
||||||
|
Guard.direction = dir;
|
||||||
|
Guard.charid = CHARID_2_GUARD;
|
||||||
|
Guard.sword = 0; /* sword_0_sheathed */
|
||||||
|
Guard.alive = -1;
|
||||||
|
Guard.action = 1; /* actions_1_run_jump */
|
||||||
|
Guard.fall_x = Guard.fall_y = 0;
|
||||||
|
/* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
|
||||||
|
* всё, что вне диапазона, оригинал сводит к 3. */
|
||||||
|
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
|
||||||
|
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
|
||||||
|
* extrastrength (seg002:38) ненулевой только у skill 4 (=1). */
|
||||||
|
guardhp_max = guardhp_curr =
|
||||||
|
(uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0));
|
||||||
|
guardhp_delta = 0;
|
||||||
|
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
|
||||||
|
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
|
||||||
|
* ровно там, где лёг (порт ветки guards_seq_hi != 0). */
|
||||||
|
{
|
||||||
|
uint16_t saved_seq = pop_guard_state_seq(room);
|
||||||
|
/* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620)
|
||||||
|
* пересчитывает её из колонки только при загрузке уровня, а дальше
|
||||||
|
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
|
||||||
|
* тайла. */
|
||||||
|
if (saved_seq) Guard.x = pop_guard_state_x(room);
|
||||||
|
/* ...и КОЛОНКУ пересчитываем из X (enter_guard, seg002:180:
|
||||||
|
* `Char.curr_col = get_tile_div_mod_m7(Char.x)`). Брать её из
|
||||||
|
* тайла нельзя: leave_guard сохраняет get_tilepos(0, row), то
|
||||||
|
* есть колонку 0 всегда. У живого стража это незаметно (X там
|
||||||
|
* сам считается из колонки), а у ЗАПОМНЕННОГО ТРУПА X
|
||||||
|
* настоящая, и curr_col оставался нулём при теле, лежащем в
|
||||||
|
* колонке 2. Тайл трупа шёл в обходе раньше тайла Кида, и Кид
|
||||||
|
* рисовался ПОВЕРХ тела вместо того, чтобы пройти за ним
|
||||||
|
* (BUG-DRAWORDER-1). Колонка нужна не только отрисовке — на
|
||||||
|
* неё смотрят коллизия и check_can_guard_see_kid. */
|
||||||
|
Guard.curr_col = guard_col_from_x(Guard.x);
|
||||||
|
pop_loadshad();
|
||||||
|
if (saved_seq) Char.curr_seq = saved_seq;
|
||||||
|
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
|
||||||
|
pop_saveshad();
|
||||||
|
pop_guard_play(); /* до первого кадра */
|
||||||
|
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
|
||||||
|
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
|
||||||
|
Guard.alive = 1;
|
||||||
|
guardhp_curr = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* pop_guard.h — СОСТОЯНИЕ стража и точки входа в его логику.
|
||||||
|
*
|
||||||
|
* Разделение по окнам (docs/layout_plan_v2.md §4, подтверждено пробником
|
||||||
|
* tests/w3bankgfx):
|
||||||
|
* - СОСТОЯНИЕ живёт здесь, в W1/W2. Писучие статики __banked-модуля
|
||||||
|
* линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк —
|
||||||
|
* только код;
|
||||||
|
* - ЛОГИКА (ИИ, боёвка) — в guards.c, собираемом как --bank 1=guards.c;
|
||||||
|
* - ОТРИСОВКА стража — в pop_gdraw.c, РЕЗИДЕНТЕ W3 (--w3): её зовёт
|
||||||
|
* только главный цикл, а из банка резидент недостижим. Так код
|
||||||
|
* отрисовки не занимает дефицитные W1/W2 (куча).
|
||||||
|
*
|
||||||
|
* Поля char_type — как в оригинале (types.h:302), чтобы порт seg005/seg006
|
||||||
|
* ложился один в один.
|
||||||
|
*/
|
||||||
|
#ifndef POP_GUARD_H
|
||||||
|
#define POP_GUARD_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "kid_data.h" /* kframe */
|
||||||
|
#include "pop_char.h"
|
||||||
|
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
|
||||||
|
|
||||||
|
/* seqids (types.h:1052+) — последовательности боя и стойки. */
|
||||||
|
#define SEQ_55_DRAW_SWORD 55
|
||||||
|
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
|
||||||
|
#define SEQ_57_BACK_WITH_SWORD 57
|
||||||
|
#define SEQ_58_GUARD_STRIKE 58
|
||||||
|
#define SEQ_60_TURN_WITH_SWORD 60
|
||||||
|
#define SEQ_61_PARRY_AFTER_STRIKE 61
|
||||||
|
#define SEQ_62_PARRY 62
|
||||||
|
#define SEQ_66_STRIKE_AFTER_PARRY 66
|
||||||
|
#define SEQ_71_DYING 71
|
||||||
|
#define SEQ_75_STRIKE 75 /* удар (Кид) */
|
||||||
|
#define SEQ_77_GUARD_STAND_INACTIVE 77
|
||||||
|
#define SEQ_80_STAND_FLIPPED 80
|
||||||
|
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
|
||||||
|
#define SEQ_87_GUARD_BECOME_INACT 87
|
||||||
|
#define SEQ_90_EN_GARDE 90
|
||||||
|
#define SEQ_92_PUT_SWORD_AWAY 92
|
||||||
|
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
|
||||||
|
|
||||||
|
/* frameids (types.h:911+) и состояния меча (types.h:332). */
|
||||||
|
#define FRAME_150_PARRY 150
|
||||||
|
#define FRAME_151_STRIKE_1 151
|
||||||
|
#define FRAME_152_STRIKE_2 152
|
||||||
|
#define FRAME_153_STRIKE_3 153
|
||||||
|
#define FRAME_157_WALK_WITH_SWORD 157
|
||||||
|
#define FRAME_158_STAND_WITH_SWORD 158
|
||||||
|
#define FRAME_161_PARRY 161
|
||||||
|
#define FRAME_162_BLOCK_TO_STRIKE 162
|
||||||
|
#define FRAME_165_WALK_WITH_SWORD 165
|
||||||
|
#define FRAME_166_STAND_INACTIVE 166
|
||||||
|
#define FRAME_167_BLOCKED 167
|
||||||
|
#define FRAME_168_BACK 168
|
||||||
|
#define FRAME_169_BEGIN_BLOCK 169
|
||||||
|
#define FRAME_170_STAND_WITH_SWORD 170
|
||||||
|
#define FRAME_171_STAND_WITH_SWORD 171
|
||||||
|
#define SWORD_0_SHEATHED 0
|
||||||
|
#define SWORD_2_DRAWN 2
|
||||||
|
|
||||||
|
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
|
||||||
|
#define CHARID_0_KID 0
|
||||||
|
#define CHARID_1_SHADOW 1
|
||||||
|
#define CHARID_2_GUARD 2
|
||||||
|
#define CHARID_4_SKELETON 4
|
||||||
|
#define CHARID_5_PRINCESS 5
|
||||||
|
|
||||||
|
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
|
||||||
|
#define NUM_GUARD_SKILLS 12
|
||||||
|
|
||||||
|
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
|
||||||
|
|
||||||
|
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
|
||||||
|
extern uint8_t guardhp_curr, guardhp_max;
|
||||||
|
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
|
||||||
|
|
||||||
|
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
|
||||||
|
* 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра),
|
||||||
|
* 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
|
||||||
|
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
|
||||||
|
extern int8_t can_guard_see_kid;
|
||||||
|
|
||||||
|
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
|
||||||
|
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
|
||||||
|
* снимает сам ИИ. */
|
||||||
|
extern int8_t is_guard_notice;
|
||||||
|
|
||||||
|
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
|
||||||
|
* кадр в autocontrol_opponent — как в оригинале.
|
||||||
|
* guard_skill — 0..11, из данных уровня; индекс в таблицы вероятностей;
|
||||||
|
* guard_refrac — «отдышка» стража: пока >0, он не атакует и не наступает;
|
||||||
|
* justblocked — Кид только что парировал (влияет на вероятность блока);
|
||||||
|
* kid_sword_strike — Кид только что бил (стража это придерживает). */
|
||||||
|
extern uint8_t guard_skill;
|
||||||
|
extern int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||||
|
|
||||||
|
/* Состояние меча КИДА (seg005/seg006):
|
||||||
|
* offguard — меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
|
||||||
|
* достаёт клинок автоматически, даже видя стража;
|
||||||
|
* holding_sword — клинок в руке (для индикатора HP стража). */
|
||||||
|
extern int8_t offguard, holding_sword;
|
||||||
|
|
||||||
|
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
|
||||||
|
extern pop_rnd_t pop_fight_seed;
|
||||||
|
|
||||||
|
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
|
||||||
|
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
|
||||||
|
void pop_check_can_guard_see_kid(void) __banked;
|
||||||
|
|
||||||
|
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
|
||||||
|
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
|
||||||
|
void pop_check_sword_hurting(void) __banked;
|
||||||
|
void pop_check_sword_hurt(void) __banked;
|
||||||
|
|
||||||
|
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
|
||||||
|
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
||||||
|
extern kframe pop_gframe;
|
||||||
|
void pop_guard_load_frame(void);
|
||||||
|
|
||||||
|
/* Есть ли живой страж в отрисованной комнате (для главного цикла). */
|
||||||
|
uint8_t pop_guard_present(void);
|
||||||
|
|
||||||
|
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
|
||||||
|
* 0 — OK, -1 — ошибка (тогда страж просто не рисуется). */
|
||||||
|
int pop_guard_load(void) __banked;
|
||||||
|
void pop_guard_free(void) __banked;
|
||||||
|
|
||||||
|
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
|
||||||
|
* seg002:0112) либо убрать его, если в комнате стража нет. */
|
||||||
|
void pop_guard_enter(uint8_t room);
|
||||||
|
|
||||||
|
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
|
||||||
|
* enter_room, пока Guard ещё держит прежнего. */
|
||||||
|
void pop_guard_leave(void);
|
||||||
|
|
||||||
|
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
|
||||||
|
* как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c
|
||||||
|
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
|
||||||
|
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
|
||||||
|
* ПОСЛЕ персонажей — она ничего не перекрывает. */
|
||||||
|
void pop_hp_draw(void) __banked;
|
||||||
|
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
|
||||||
|
void pop_hp_invalidate(void) __banked;
|
||||||
|
|
||||||
|
void pop_guard_heal(void) __banked;
|
||||||
|
void pop_guard_draw(void) __banked;
|
||||||
|
|
||||||
|
/* Ширина спрайта последнего нарисованного кадра стража (char_width_half
|
||||||
|
* в set_char_collision). Нужна для его ТАЙЛА ОБЪЕКТА — от него зависит
|
||||||
|
* порядок отрисовки относительно Кида. */
|
||||||
|
uint16_t pop_guard_fp_width(void) __banked;
|
||||||
|
|
||||||
|
/* Убрать стража со сцены (смена комнаты / инициализация). */
|
||||||
|
void pop_guard_reset(void);
|
||||||
|
|
||||||
|
/* Чит «убить стража» (seg000:786, K): скелета не берёт. Вызывать только
|
||||||
|
* при включённых читах — проверку делает вызывающий. */
|
||||||
|
void pop_guard_kill(void);
|
||||||
|
|
||||||
|
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
|
||||||
|
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */
|
||||||
|
void pop_do_delta_hp(void);
|
||||||
|
|
||||||
|
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
|
||||||
|
void pop_guard_tick(void) __banked;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -2,61 +2,135 @@
|
|||||||
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
|
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
|
||||||
* seqtbl-интерпретатор. Данные — kid_data.h.
|
* seqtbl-интерпретатор. Данные — kid_data.h.
|
||||||
*/
|
*/
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprinter_mem.h>
|
||||||
#include <graphics.h>
|
#include <graphics.h>
|
||||||
#include <gfx.h>
|
#include <gfx.h>
|
||||||
#include <sprite.h>
|
#include <sprite.h>
|
||||||
#include "pop_kid.h"
|
#include "pop_kid.h"
|
||||||
|
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
|
||||||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||||||
|
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
|
||||||
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
||||||
|
#include "pop_geom.h"
|
||||||
#include "kid_data.h"
|
#include "kid_data.h"
|
||||||
|
|
||||||
kid_t Kid;
|
pop_char_t Kid;
|
||||||
|
|
||||||
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
|
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
|
||||||
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
||||||
int8_t knock;
|
int8_t knock;
|
||||||
|
|
||||||
/* Координатные таблицы (data.h). */
|
/* Порт глобала next_level: seqtbl-команда END_LEVEL инкрементит его в конце
|
||||||
static const uint8_t x_bump[20] = {
|
* seq_70 (подъём по лестнице за дверью уровня), а главный цикл сравнивает с
|
||||||
(uint8_t)-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
|
* текущим уровнем и грузит следующий. Счётчик, а не флаг — ровно как в
|
||||||
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
|
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
|
||||||
};
|
* легли сюда же без переделки. */
|
||||||
static const int16_t y_land[5] = {-8, 55, 118, 181, 244};
|
uint8_t pop_next_level;
|
||||||
#define FIRST_ONSCREEN_COLUMN 5
|
|
||||||
#define TILE_SIZEX 14
|
|
||||||
|
|
||||||
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
|
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
|
||||||
#define KID_MAXPAGES 28
|
#define KID_MAXPAGES 28
|
||||||
static atlas_t kidp[KID_MAXPAGES];
|
static atlas_t kidp[KID_MAXPAGES];
|
||||||
static uint8_t kid_npages;
|
static uint8_t kid_npages;
|
||||||
|
|
||||||
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): отдельный
|
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
|
||||||
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237).
|
* ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
|
||||||
* Из sword_tbl взяты только строки 35..42 — те, что адресуют эти кадры (бой
|
* Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
|
||||||
* не портирован); индекс в атласе = sword_frame − 35. Смещения таблицы —
|
* sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
|
||||||
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */
|
* Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
|
||||||
#define SWORD_SF_LO 35
|
* calc_screen_x_coord, x — «вперёд» по направлению персонажа. */
|
||||||
#define SWORD_SF_HI 42
|
static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
|
||||||
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 };
|
static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
|
||||||
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 };
|
static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
|
||||||
static atlas_t swordp;
|
static atlas_t swordp;
|
||||||
static uint8_t sword_ok;
|
static uint8_t sword_ok;
|
||||||
|
|
||||||
int pop_sword_load(void)
|
int pop_sword_load(void)
|
||||||
{
|
{
|
||||||
if (atlas_load(&swordp, "sword.atl") != 0) return -1;
|
if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
|
||||||
sword_ok = 1;
|
sword_ok = 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в
|
||||||
|
* кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
|
||||||
|
* в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем
|
||||||
|
* замапленной — вызывающий делает gfx_w0_unmap сам. */
|
||||||
|
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
|
||||||
|
int obj_x, int obj_y,
|
||||||
|
int *rx, int *ry, uint16_t *rw, uint16_t *rh)
|
||||||
|
{
|
||||||
|
uint8_t sf, id, flip;
|
||||||
|
const uint8_t *simg;
|
||||||
|
uint16_t w, h;
|
||||||
|
int bx, top;
|
||||||
|
|
||||||
|
if (!sword_ok) return 0;
|
||||||
|
if (!((ch->frame >= 229 && ch->frame < 238) ||
|
||||||
|
ch->sword != 0 ||
|
||||||
|
(ch->charid == CHARID_2_GUARD && ch->alive < 0)))
|
||||||
|
return 0;
|
||||||
|
sf = (uint8_t)(fr->sword & 0x3F);
|
||||||
|
if (sf == 0 || sf >= KID_NSWORD) return 0;
|
||||||
|
id = SWORD_ID[sf];
|
||||||
|
if (id == 0xFF) return 0;
|
||||||
|
|
||||||
|
simg = (const uint8_t *)atlas_image(&swordp, id);
|
||||||
|
gfx_w0_map(swordp.page);
|
||||||
|
w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
|
||||||
|
h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
|
||||||
|
if (!w || !h) return 0;
|
||||||
|
|
||||||
|
flip = (uint8_t)(ch->direction >= 0);
|
||||||
|
bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
|
||||||
|
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
|
||||||
|
if (flip) bx -= (int)w;
|
||||||
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
|
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
|
||||||
|
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
|
||||||
|
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
|
||||||
|
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0);
|
||||||
|
else
|
||||||
|
gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0);
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
*rx = bx; *ry = top; *rw = w; *rh = h;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y).
|
||||||
|
* Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры
|
||||||
|
* Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */
|
||||||
|
void pop_kid_img_blit(uint8_t image, int x, int y)
|
||||||
|
{
|
||||||
|
uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */
|
||||||
|
uint8_t idx = (uint8_t)(image & 7);
|
||||||
|
const uint8_t *img;
|
||||||
|
uint16_t w, h;
|
||||||
|
if (pg >= kid_npages) return;
|
||||||
|
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
|
||||||
|
gfx_w0_map(kidp[pg].page);
|
||||||
|
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||||
|
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||||
|
/* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid —
|
||||||
|
* ради бесплатного горизонтального флипа), поэтому блит обязан быть
|
||||||
|
* колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной.
|
||||||
|
* noclip БЕЗ проверки (CLIP-1): единственный вызывающий — полоса HP
|
||||||
|
* (pop_gdraw.c), у неё координаты фиксированные и заведомо на экране. */
|
||||||
|
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
|
||||||
int pop_kid_load(uint8_t npages)
|
int pop_kid_load(uint8_t npages)
|
||||||
{
|
{
|
||||||
static const char *const names[KID_MAXPAGES] = {
|
static const char *const names[KID_MAXPAGES] = {
|
||||||
"kid0.atl","kid1.atl","kid2.atl","kid3.atl","kid4.atl","kid5.atl",
|
"KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
|
||||||
"kid6.atl","kid7.atl","kid8.atl","kid9.atl","kid10.atl","kid11.atl",
|
"KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
|
||||||
"kid12.atl","kid13.atl","kid14.atl","kid15.atl","kid16.atl","kid17.atl",
|
"KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
|
||||||
"kid18.atl","kid19.atl","kid20.atl","kid21.atl","kid22.atl","kid23.atl",
|
"KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
|
||||||
"kid24.atl","kid25.atl","kid26.atl","kid27.atl"
|
"KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
|
||||||
};
|
};
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
|
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
|
||||||
@@ -75,63 +149,273 @@ void pop_kid_free(void)
|
|||||||
kid_npages = 0;
|
kid_npages = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- seqtbl-доступ (база SEQTBL_BASE) ------------------------------ */
|
/* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
|
||||||
#define SEQ(cs) kid_seqtbl[(uint16_t)((cs) - SEQTBL_BASE)]
|
* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
|
||||||
|
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
|
||||||
|
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
|
||||||
|
* весь остальной код читает cur_frame — 5-байтовую копию текущего кадра.
|
||||||
|
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
|
||||||
|
* и коллизия/отрисовка работают с ним, а не с таблицей.
|
||||||
|
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
||||||
|
extern void _gfx_w0_isr(void);
|
||||||
|
|
||||||
|
#define KD_DATA_OFF 0x100u
|
||||||
|
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
|
||||||
|
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
|
||||||
|
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
|
||||||
|
* берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
|
||||||
|
* (`ld c,l` / `inc b`). Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
|
||||||
|
* −256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
|
||||||
|
* пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
|
||||||
|
* Артефакт: .sprinter-cc-roomtest/pop_kid.asm, функция load_frame. */
|
||||||
|
static const uint8_t *kd_frame_ptr(uint8_t i)
|
||||||
|
{
|
||||||
|
uint16_t off = (uint16_t)i;
|
||||||
|
off += (uint16_t)(off << 2); /* i*5 = i + i*4, без умножения */
|
||||||
|
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
|
||||||
|
return (const uint8_t *)off;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
|
||||||
|
* страницу данных, см. pop_extract_kid_data.py). */
|
||||||
|
static const uint8_t *kd_gframe_ptr(uint8_t i)
|
||||||
|
{
|
||||||
|
uint16_t off = (uint16_t)i;
|
||||||
|
off += (uint16_t)(off << 2);
|
||||||
|
off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
|
||||||
|
return (const uint8_t *)off;
|
||||||
|
}
|
||||||
|
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
|
||||||
|
(uint16_t)((cs) - SEQTBL_BASE)))
|
||||||
|
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
|
||||||
|
* страницей данных — то есть внутри play_seq. */
|
||||||
|
#define SEQ(cs) KD_SEQ(cs)
|
||||||
|
|
||||||
|
static uint8_t kdat_blk, kdat_page, kdat_ok;
|
||||||
|
|
||||||
|
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
|
||||||
|
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
|
||||||
|
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
|
||||||
|
* тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
|
||||||
|
static kframe cur_frame;
|
||||||
|
static kframe kid_frame; /* кадр Kid для kid_draw */
|
||||||
|
|
||||||
|
/* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в
|
||||||
|
* оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
|
||||||
|
* стоят loadkid/savekid и loadshad/saveshad (seg006:809..825). Копия, а
|
||||||
|
* не указатель, — и так же, как в оригинале, и быстрее на Z80: обращения
|
||||||
|
* в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
|
||||||
|
* копирование платится один раз на переключение персонажа. */
|
||||||
|
pop_char_t Char;
|
||||||
|
|
||||||
|
/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
|
||||||
|
* рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
|
||||||
|
* ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
|
||||||
|
* ними: для стража Opp = Kid, для Кида Opp = Guard. */
|
||||||
|
pop_char_t Opp;
|
||||||
|
|
||||||
|
/* Снимок последовательности на момент входа в окно Char: по нему
|
||||||
|
* pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)»
|
||||||
|
* от «её сменил pop_map (прямо в Kid)». */
|
||||||
|
static uint16_t kid_seq_at_load;
|
||||||
|
|
||||||
|
void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; }
|
||||||
|
void pop_savekid(void) { Kid = Char; }
|
||||||
|
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально
|
||||||
|
* меняет у активного персонажа — последовательность и состояние меча.
|
||||||
|
*
|
||||||
|
* Почему не `Kid = Char`, как делает savekid оригинала: у нас часть
|
||||||
|
* действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq,
|
||||||
|
* safe_step...), и он пишет ПРЯМО в Kid — там ещё не портирован переход на
|
||||||
|
* Char. Полное копирование затирало эти правки: Kid терял выравнивание x
|
||||||
|
* и ряд, и спуск с уступа через вис не срабатывал — персонаж просто
|
||||||
|
* приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */
|
||||||
|
void pop_savekid_state(void)
|
||||||
|
{
|
||||||
|
/* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char
|
||||||
|
* (pop_char_set_seq), pop_map — прямо в Kid (kid_set_seq: спуск с
|
||||||
|
* уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше:
|
||||||
|
* диспетчер про эти переходы не знает и, копируя Char вслепую, затирал
|
||||||
|
* их — спуск в проём не срабатывал, а персонаж даже не приседал, потому
|
||||||
|
* что pop_down_action уже вернул «обработано». */
|
||||||
|
uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load);
|
||||||
|
if (!map_changed) Kid.curr_seq = Char.curr_seq;
|
||||||
|
Kid.sword = Char.sword;
|
||||||
|
}
|
||||||
|
void pop_loadshad(void) { Char = Guard; }
|
||||||
|
void pop_saveshad(void) { Guard = Char; }
|
||||||
|
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
|
||||||
|
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
|
||||||
|
void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; }
|
||||||
|
void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; }
|
||||||
|
|
||||||
|
/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
|
||||||
|
* +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
|
||||||
|
* приходится на ширину тайла между ними. Живёт здесь, а не в guards.c,
|
||||||
|
* потому что нужен по обе стороны банковой границы — и ИИ, и диспетчеру
|
||||||
|
* боёвки (pop_ctrl). */
|
||||||
|
int16_t pop_char_opp_dist(void)
|
||||||
|
{
|
||||||
|
int16_t distance;
|
||||||
|
if (Char.room != Opp.room) return 999;
|
||||||
|
distance = (int16_t)Opp.x - (int16_t)Char.x;
|
||||||
|
if (Char.direction < 0) distance = -distance;
|
||||||
|
if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pop_kid_data_load(const char *path)
|
||||||
|
{
|
||||||
|
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
||||||
|
uint8_t saved_w3, blk;
|
||||||
|
uint16_t stub;
|
||||||
|
int fd, n;
|
||||||
|
|
||||||
|
fd = open(path, O_RDONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
blk = mem_alloc_pages(1);
|
||||||
|
if (!blk) { close(fd); return -1; }
|
||||||
|
kdat_blk = blk;
|
||||||
|
kdat_page = mem_get_page(blk, 0);
|
||||||
|
|
||||||
|
saved_w3 = _io_page_w3;
|
||||||
|
sprinter_page_w3(kdat_page);
|
||||||
|
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
|
||||||
|
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||||
|
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
|
||||||
|
pg[0x38] = 0xC3;
|
||||||
|
pg[0x39] = (uint8_t)(stub & 0xFF);
|
||||||
|
pg[0x3A] = (uint8_t)(stub >> 8);
|
||||||
|
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
|
||||||
|
}
|
||||||
|
sprinter_page_w3(saved_w3);
|
||||||
|
close(fd);
|
||||||
|
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
|
||||||
|
kdat_ok = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_kid_data_free(void)
|
||||||
|
{
|
||||||
|
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
|
||||||
|
* разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF). Сама маппит страницу,
|
||||||
|
* поэтому годится для вызова извне (pop_guard) — но НЕ в горячем цикле. */
|
||||||
|
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
|
||||||
|
{
|
||||||
|
const uint8_t *f;
|
||||||
|
uint16_t off = (uint16_t)idx;
|
||||||
|
if (!kdat_ok) { out5[0] = 255; return; }
|
||||||
|
off += (uint16_t)(off << 2); /* idx*5, без умножения */
|
||||||
|
off += (uint16_t)(KD_DATA_OFF + tbl_off);
|
||||||
|
gfx_w0_map(kdat_page);
|
||||||
|
f = (const uint8_t *)off;
|
||||||
|
out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
|
||||||
|
out5[3] = f[3]; out5[4] = f[4];
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
|
||||||
|
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
|
||||||
|
static void load_frame(void)
|
||||||
|
{
|
||||||
|
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
|
||||||
|
* стража frame_tbl_guard с индексом frame + add_frame − 149, где
|
||||||
|
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||||||
|
const uint8_t *f;
|
||||||
|
if (Char.charid == CHARID_2_GUARD) {
|
||||||
|
int16_t idx = (int16_t)Char.frame;
|
||||||
|
if (idx >= 102 && idx < 107) idx += 70;
|
||||||
|
idx -= 149;
|
||||||
|
if (idx < 0 || idx >= KID_NGFRAMES) { cur_frame.image = 255; return; }
|
||||||
|
f = kd_gframe_ptr((uint8_t)idx);
|
||||||
|
} else {
|
||||||
|
f = kd_frame_ptr(Char.frame);
|
||||||
|
}
|
||||||
|
cur_frame.image = f[0];
|
||||||
|
cur_frame.dx = (int8_t)f[1];
|
||||||
|
cur_frame.dy = (int8_t)f[2];
|
||||||
|
cur_frame.flags = f[3];
|
||||||
|
cur_frame.sword = f[4];
|
||||||
|
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
|
||||||
|
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
|
||||||
|
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
|
||||||
|
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
|
||||||
|
* на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа
|
||||||
|
* Кида и рисовал произвольную позу. Теперь владелец один — тот, для
|
||||||
|
* кого кадр реально загружен. */
|
||||||
|
if (Char.charid == CHARID_0_KID) kid_frame = cur_frame;
|
||||||
|
else pop_gframe = cur_frame;
|
||||||
|
}
|
||||||
|
|
||||||
static uint8_t char_dx_forward(int8_t dx)
|
static uint8_t char_dx_forward(int8_t dx)
|
||||||
{
|
{
|
||||||
if (Kid.direction < 0) dx = (int8_t)-dx;
|
if (Char.direction < 0) dx = (int8_t)-dx;
|
||||||
return (uint8_t)(Kid.x + dx);
|
return (uint8_t)(Char.x + dx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Аксессоры к текущему кадру (для pop_map — без дублирования kid_frames). */
|
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
|
||||||
int8_t kid_cur_dx(void) { return kid_frames[Kid.frame].dx; }
|
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
|
||||||
uint8_t kid_cur_flags(void) { return kid_frames[Kid.frame].flags; }
|
int8_t kid_cur_dx(void) { return cur_frame.dx; }
|
||||||
|
int8_t kid_cur_dy(void) { return cur_frame.dy; }
|
||||||
|
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
|
||||||
|
|
||||||
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
|
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
|
||||||
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
|
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
|
||||||
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
|
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
|
||||||
|
|
||||||
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0). */
|
/* То же для АКТИВНОГО персонажа — внутри окна Char (порт
|
||||||
|
* seqtbl_offset_char, seg006). Нужен стражу: его последовательность
|
||||||
|
* ставится, когда Char уже загружен из Guard. */
|
||||||
|
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }
|
||||||
|
|
||||||
|
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
|
||||||
|
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
|
||||||
|
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
|
||||||
|
* load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
|
||||||
void play_seq(void)
|
void play_seq(void)
|
||||||
{
|
{
|
||||||
|
if (!kdat_ok) return;
|
||||||
|
gfx_w0_map(kdat_page);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
uint8_t cmd = SEQ(Kid.curr_seq);
|
uint8_t cmd = SEQ(Char.curr_seq);
|
||||||
Kid.curr_seq++;
|
Char.curr_seq++;
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 0xFB: /* DX */
|
case 0xFB: /* DX */
|
||||||
Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq));
|
Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
|
||||||
Kid.curr_seq++;
|
Char.curr_seq++;
|
||||||
break;
|
break;
|
||||||
case 0xFA: /* DY */
|
case 0xFA: /* DY */
|
||||||
Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq));
|
Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
|
||||||
Kid.curr_seq++;
|
Char.curr_seq++;
|
||||||
break;
|
break;
|
||||||
case 0xFE: /* FLIP */
|
case 0xFE: /* FLIP */
|
||||||
Kid.direction = (int8_t)~Kid.direction;
|
Char.direction = (int8_t)~Char.direction;
|
||||||
break;
|
break;
|
||||||
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
|
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
|
||||||
Kid.curr_seq += 2;
|
Char.curr_seq += 2;
|
||||||
break;
|
break;
|
||||||
case 0xFF: { /* JMP */
|
case 0xFF: { /* JMP */
|
||||||
uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) |
|
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
|
||||||
((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8));
|
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
|
||||||
Kid.curr_seq = a;
|
Char.curr_seq = a;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 0xFD: /* UP */
|
case 0xFD: /* UP */
|
||||||
Kid.curr_row--;
|
Char.curr_row--;
|
||||||
break;
|
break;
|
||||||
case 0xFC: /* DOWN */
|
case 0xFC: /* DOWN */
|
||||||
Kid.curr_row++;
|
Char.curr_row++;
|
||||||
break;
|
break;
|
||||||
case 0xF9: /* ACTION */
|
case 0xF9: /* ACTION */
|
||||||
Kid.action = SEQ(Kid.curr_seq);
|
Char.action = SEQ(Char.curr_seq);
|
||||||
Kid.curr_seq++;
|
Char.curr_seq++;
|
||||||
break;
|
break;
|
||||||
case 0xF8: /* SET_FALL */
|
case 0xF8: /* SET_FALL */
|
||||||
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
|
Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||||||
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
|
Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||||||
break;
|
break;
|
||||||
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
|
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
|
||||||
knock = 1;
|
knock = 1;
|
||||||
@@ -139,42 +423,97 @@ void play_seq(void)
|
|||||||
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
|
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
|
||||||
knock = -1;
|
knock = -1;
|
||||||
break;
|
break;
|
||||||
case 0xF2: /* SOUND — потребить 1 байт */
|
case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт
|
||||||
Kid.curr_seq++;
|
* ВТОРОЙ смысл — «Кид нашумел», и его терять нельзя.
|
||||||
|
* Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0
|
||||||
|
* назван silent потому, что НЕ звучит, а стражи его всё
|
||||||
|
* равно замечают (стоит, например, в seq доставания меча).
|
||||||
|
* DRINK(3)/LEVEL(4) — не шум. См. BUG-GUARD-DEAF-1. */
|
||||||
|
if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1;
|
||||||
|
Char.curr_seq++;
|
||||||
break;
|
break;
|
||||||
case 0xF1: /* END_LEVEL */
|
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
|
||||||
|
* В оригинале это `++next_level`, а главный цикл
|
||||||
|
* (play_level_2, seg003:0386) выходит, когда next_level
|
||||||
|
* разошёлся с current_level. У нас так же: счётчик, а
|
||||||
|
* решение — снаружи (roomtest.c). */
|
||||||
|
pop_next_level++;
|
||||||
break;
|
break;
|
||||||
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
|
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
|
||||||
* (порт play_seq -> proc_get_object, seg006) */
|
* (порт play_seq -> proc_get_object, seg006) */
|
||||||
if (SEQ(Kid.curr_seq) == 1)
|
if (SEQ(Char.curr_seq) == 1)
|
||||||
pop_proc_get_object();
|
pop_proc_get_object();
|
||||||
Kid.curr_seq++;
|
Char.curr_seq++;
|
||||||
break;
|
break;
|
||||||
case 0xF6: /* DIE nop */
|
case 0xF6: /* DIE nop */
|
||||||
break;
|
break;
|
||||||
default: /* КАДР */
|
default: /* КАДР */
|
||||||
Kid.frame = cmd;
|
Char.frame = cmd;
|
||||||
|
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
|
||||||
|
gfx_w0_unmap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* load_fram_det_col (seg006:0144) = load_frame() + determine_col().
|
||||||
|
*
|
||||||
|
* Зачем это отдельной публичной точкой. cur_frame — ОДИН глобал на всех
|
||||||
|
* персонажей (так и в оригинале), и его владелец — тот, кто последним
|
||||||
|
* прошёл load_frame. В кадре последним тикает страж, поэтому к моменту
|
||||||
|
* управления Кидом в cur_frame лежит кадр СТРАЖА. А через cur_frame
|
||||||
|
* считается вся геометрия: dx_weight -> determine_col ->
|
||||||
|
* distance_to_edge_weight -> get_edge_distance, то есть буквально выбор
|
||||||
|
* ветки в check_jump_up.
|
||||||
|
*
|
||||||
|
* Оригинал от этого страхуется явно: play_kid_frame (seg000:1209) и
|
||||||
|
* play_guard_frame (seg000:1246) сразу после loadkid/loadshad зовут
|
||||||
|
* load_fram_det_col() — ДО control(). У нас этого не было, и Кид считал
|
||||||
|
* свой прыжок по кадру стража (BUG-CTRL-FRAME-1): при distance 2 вместо 10
|
||||||
|
* check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и
|
||||||
|
* зацеп». Результат менялся от кадра к кадру — вместе с анимацией стража.
|
||||||
|
*
|
||||||
|
* determine_col у нас существует только для Кида: pop_map работает с Kid
|
||||||
|
* напрямую, а не с абстрактным Char. Поэтому вторую половину связки делаем
|
||||||
|
* только на его ветке — стражу пока хватает перезагрузки самого кадра (его
|
||||||
|
* колонку ведёт pop_guard_enter/leave). Char.curr_col синхронизируем
|
||||||
|
* руками: окно Char — копия Kid. */
|
||||||
|
void pop_load_fram_det_col(void)
|
||||||
|
{
|
||||||
|
if (!kdat_ok) return;
|
||||||
|
gfx_w0_map(kdat_page);
|
||||||
|
load_frame();
|
||||||
|
gfx_w0_unmap();
|
||||||
|
if (Char.charid == CHARID_0_KID) {
|
||||||
|
pop_determine_col();
|
||||||
|
Char.curr_col = Kid.curr_col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
|
||||||
|
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
|
||||||
|
* работает с Char и сам по себе не знает, чей это персонаж. */
|
||||||
|
void pop_kid_play(void) { pop_loadkid(); play_seq(); pop_savekid(); }
|
||||||
|
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }
|
||||||
|
|
||||||
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
|
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
|
||||||
{
|
{
|
||||||
Kid.curr_col = col;
|
Kid.curr_col = col;
|
||||||
Kid.curr_row = row;
|
Kid.curr_row = row;
|
||||||
Kid.direction = dir;
|
Kid.direction = dir;
|
||||||
Kid.x = (uint8_t)(x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||||||
Kid.y = (uint8_t)y_land[row + 1];
|
Kid.y = (uint8_t)pop_y_land[row + 1];
|
||||||
Kid.fall_x = Kid.fall_y = 0;
|
Kid.fall_x = Kid.fall_y = 0;
|
||||||
Kid.repeat = 0;
|
Kid.repeat = 0;
|
||||||
|
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
|
||||||
|
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
|
||||||
Kid.curr_seq = kid_seq_off[seq_id];
|
Kid.curr_seq = kid_seq_off[seq_id];
|
||||||
play_seq(); /* до первого кадра */
|
pop_kid_play(); /* до первого кадра */
|
||||||
}
|
}
|
||||||
|
|
||||||
void kid_tick(void)
|
void kid_tick(void)
|
||||||
{
|
{
|
||||||
play_seq();
|
pop_kid_play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
|
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
|
||||||
@@ -204,6 +543,64 @@ static int kid_lx[2], kid_ly[2];
|
|||||||
static uint16_t kid_lw[2], kid_lh[2];
|
static uint16_t kid_lw[2], kid_lh[2];
|
||||||
static uint8_t kid_lvalid[2];
|
static uint8_t kid_lvalid[2];
|
||||||
|
|
||||||
|
/* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона).
|
||||||
|
* Раньше он объединялся с прямоугольником персонажа — но объединение сильно
|
||||||
|
* больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил
|
||||||
|
* почти вдвое шире и выше самого Kid, а heal стоит ровно по площади.
|
||||||
|
* Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение
|
||||||
|
* оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования —
|
||||||
|
* зато клинок/брызги честно уходят под передние грани тайлов). */
|
||||||
|
static int kid_ox[2], kid_oy[2];
|
||||||
|
static uint16_t kid_ow[2], kid_oh[2];
|
||||||
|
static uint8_t kid_ovalid[2];
|
||||||
|
|
||||||
|
/* Добавить накладной спрайт в overlay-прямоугольник страницы dp. Клинок и
|
||||||
|
* брызги в одном кадре встречаются редко и оба маленькие — их объединение
|
||||||
|
* дешевле третьего слота. */
|
||||||
|
static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h)
|
||||||
|
{
|
||||||
|
if (!kid_ovalid[dp]) {
|
||||||
|
kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h;
|
||||||
|
kid_ovalid[dp] = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
int x0 = kid_ox[dp], y0 = kid_oy[dp];
|
||||||
|
int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_oh[dp];
|
||||||
|
if (x < x0) x0 = x;
|
||||||
|
if (y < y0) y0 = y;
|
||||||
|
if (x + (int)w > x1) x1 = x + (int)w;
|
||||||
|
if (y + (int)h > y1) y1 = y + (int)h;
|
||||||
|
kid_ox[dp] = x0; kid_oy[dp] = y0;
|
||||||
|
kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Окно fore-клипа = объединение «персонаж + накладные». Считается по
|
||||||
|
* прямоугольникам, ничего не рисует. */
|
||||||
|
static void kid_fore_clip(uint8_t dp)
|
||||||
|
{
|
||||||
|
int x0, y0, x1, y1;
|
||||||
|
if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; }
|
||||||
|
if (!kid_ovalid[dp]) {
|
||||||
|
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
|
||||||
|
(int)kid_lw[dp], (int)kid_lh[dp]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!kid_lvalid[dp]) {
|
||||||
|
pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF,
|
||||||
|
(int)kid_ow[dp], (int)kid_oh[dp]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp];
|
||||||
|
y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp];
|
||||||
|
x1 = kid_lx[dp] + (int)kid_lw[dp];
|
||||||
|
y1 = kid_ly[dp] + (int)kid_lh[dp];
|
||||||
|
if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp];
|
||||||
|
if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp];
|
||||||
|
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
|
||||||
|
}
|
||||||
|
|
||||||
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
|
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
|
||||||
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
|
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
|
||||||
static int kid_fp_x, kid_fp_y;
|
static int kid_fp_x, kid_fp_y;
|
||||||
@@ -247,45 +644,52 @@ void kid_draw_splash(void)
|
|||||||
int top = obj_y - (int)h + 1;
|
int top = obj_y - (int)h + 1;
|
||||||
if (flip) bx -= (int)w;
|
if (flip) bx -= (int)w;
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
gfx_blit_cols(bx, top + POP_YOFF, img, flip);
|
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
|
||||||
|
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||||
|
else
|
||||||
|
gfx_blit_cols(bx, top + POP_YOFF, img, flip);
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
|
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
|
||||||
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе
|
/* Брызги — накладной спрайт: свой прямоугольник heal (см.
|
||||||
* брызги останутся на экране после кадра. */
|
* kid_overlay_add), окно fore пересчитываем объединением. */
|
||||||
{
|
{
|
||||||
uint8_t dp = gfx_get_draw_page();
|
uint8_t dp = gfx_get_draw_page();
|
||||||
if (kid_lvalid[dp]) {
|
kid_overlay_add(dp, bx, top, w, h);
|
||||||
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx;
|
kid_fore_clip(dp);
|
||||||
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
|
|
||||||
int x1 = kid_lx[dp] + (int)kid_lw[dp];
|
|
||||||
int y1 = kid_ly[dp] + (int)kid_lh[dp];
|
|
||||||
if (bx + (int)w > x1) x1 = bx + (int)w;
|
|
||||||
if (top + (int)h > y1) y1 = top + (int)h;
|
|
||||||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
|
||||||
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
|
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid.
|
||||||
|
*
|
||||||
|
* pop_heal_fast, а не голый gfx_heal: прямоугольник тут — это ровно тот
|
||||||
|
* прямоугольник, который kid_draw нарисовал noclip-ядром, то есть в
|
||||||
|
* подавляющем большинстве кадров он заведомо на экране. Клипающий путь
|
||||||
|
* остаётся для straddle (kid_render_dx = ∓140) и падения ниже поля —
|
||||||
|
* там координата реально уходит за экран. */
|
||||||
void kid_heal(void)
|
void kid_heal(void)
|
||||||
{
|
{
|
||||||
uint8_t p = gfx_get_draw_page();
|
uint8_t p = gfx_get_draw_page();
|
||||||
if (kid_lvalid[p])
|
if (kid_lvalid[p]) /* +YOFF: центрирование */
|
||||||
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
|
pop_heal_fast(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]);
|
||||||
kid_lvalid[p] = 0;
|
if (kid_ovalid[p])
|
||||||
|
pop_heal_fast(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]);
|
||||||
|
kid_lvalid[p] = kid_ovalid[p] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void kid_draw(void)
|
void kid_draw(void)
|
||||||
{
|
{
|
||||||
kframe fr = kid_frames[Kid.frame];
|
kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */
|
||||||
int fwd, obj_x, obj_y, top;
|
int fwd, obj_x, obj_y, top;
|
||||||
uint8_t page, idx;
|
uint8_t page, idx;
|
||||||
const uint8_t *img;
|
const uint8_t *img;
|
||||||
uint16_t w, h;
|
uint16_t w, h;
|
||||||
|
|
||||||
|
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
|
||||||
|
* кадра / страница не загружена) перекрывать нечего, и fore-проход
|
||||||
|
* не должен трогать экран. */
|
||||||
|
pop_fore_set_clip(0, 0, 0, 0);
|
||||||
if (fr.image == 255)
|
if (fr.image == 255)
|
||||||
return; /* пустой кадр */
|
return; /* пустой кадр */
|
||||||
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
|
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
|
||||||
@@ -316,7 +720,8 @@ void kid_draw(void)
|
|||||||
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
|
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
|
||||||
int bx = obj_x + KID_XOFF;
|
int bx = obj_x + KID_XOFF;
|
||||||
int skip = 0; /* строк спрайта срезано сверху */
|
int skip = 0; /* строк спрайта срезано сверху */
|
||||||
int ct;
|
int ct, cr;
|
||||||
|
uint16_t vis_w = w; /* видимая ширина (клип справа) */
|
||||||
if (flip)
|
if (flip)
|
||||||
bx -= (int)w; /* seg008: xpos -= image->w */
|
bx -= (int)w; /* seg008: xpos -= image->w */
|
||||||
top = obj_y - (int)h + 1 + KID_YOFF;
|
top = obj_y - (int)h + 1 + KID_YOFF;
|
||||||
@@ -329,9 +734,28 @@ void kid_draw(void)
|
|||||||
skip = ct - top;
|
skip = ct - top;
|
||||||
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
|
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
|
||||||
}
|
}
|
||||||
|
/* clip_char, вторая половина: правый косяк двери уровня. Дверь
|
||||||
|
* рисуется ПОД персонажем (вся в backtable), поэтому спрятать его
|
||||||
|
* можно только срезав спрайт — см. pop_clip_char_right. */
|
||||||
|
cr = pop_clip_char_right();
|
||||||
|
if (cr) {
|
||||||
|
int avail = cr - bx;
|
||||||
|
if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */
|
||||||
|
else if (avail < (int)w) vis_w = (uint16_t)avail;
|
||||||
|
}
|
||||||
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
||||||
if (skip < (int)h) /* +YOFF: центрирование */
|
if (skip < (int)h && vis_w) { /* +YOFF: центрирование */
|
||||||
gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0);
|
/* noclip-ядро ширину не ограничивает, поэтому при обрезке справа
|
||||||
|
* идём общим путём: это редкие кадры (подъём в двери уровня раз
|
||||||
|
* за уровень), цена подготовки тут не видна. */
|
||||||
|
if (vis_w == w && pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
|
||||||
|
(uint16_t)((int)h - skip)))
|
||||||
|
gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip,
|
||||||
|
(uint8_t)skip, 0);
|
||||||
|
else
|
||||||
|
gfx_blit_cols_part_w(bx, top + skip + POP_YOFF, img, flip,
|
||||||
|
skip, 0, (uint8_t)(vis_w == w ? 0 : vis_w));
|
||||||
|
}
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
|
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
|
||||||
* узко, только по прямоугольнику Кида и только при выходе. */
|
* узко, только по прямоугольнику Кида и только при выходе. */
|
||||||
@@ -339,49 +763,38 @@ void kid_draw(void)
|
|||||||
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
|
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
|
||||||
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
|
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
|
||||||
kid_lx[dp] = bx; kid_ly[dp] = top + skip;
|
kid_lx[dp] = bx; kid_ly[dp] = top + skip;
|
||||||
kid_lw[dp] = w; kid_lh[dp] = (uint16_t)((int)h - skip);
|
kid_lw[dp] = vis_w; kid_lh[dp] = (uint16_t)((int)h - skip);
|
||||||
kid_lvalid[dp] = (skip < (int)h);
|
kid_lvalid[dp] = (skip < (int)h && vis_w != 0);
|
||||||
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
|
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
|
||||||
}
|
}
|
||||||
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
|
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
|
||||||
* Kid. Прямоугольник heal расширяем объединением — иначе клинок, выйдя
|
* Kid — со СВОИМ прямоугольником heal (kid_overlay_add). */
|
||||||
* за габарит Kid, оставит след. */
|
{
|
||||||
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) {
|
int sx, sy; uint16_t sw, sh;
|
||||||
uint8_t sf = (uint8_t)(fr.sword & 0x3F);
|
if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
|
||||||
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) {
|
kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
|
||||||
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
|
|
||||||
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
|
|
||||||
uint16_t sw, sh;
|
|
||||||
gfx_w0_map(swordp.page);
|
|
||||||
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
|
|
||||||
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
|
|
||||||
if (sw && sh) {
|
|
||||||
uint8_t sflip = (Kid.direction >= 0);
|
|
||||||
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
|
|
||||||
: (int)SWORD_DX[si]);
|
|
||||||
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
|
|
||||||
uint8_t dp = gfx_get_draw_page() & 1;
|
|
||||||
if (sflip) sbx -= (int)sw;
|
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
|
||||||
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
|
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
|
||||||
if (!kid_lvalid[dp]) {
|
|
||||||
kid_lx[dp] = sbx; kid_ly[dp] = stop;
|
|
||||||
kid_lw[dp] = sw; kid_lh[dp] = sh;
|
|
||||||
kid_lvalid[dp] = 1;
|
|
||||||
} else {
|
|
||||||
int x0 = kid_lx[dp], y0 = kid_ly[dp];
|
|
||||||
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
|
|
||||||
if (sbx < x0) x0 = sbx;
|
|
||||||
if (stop < y0) y0 = stop;
|
|
||||||
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
|
|
||||||
if (stop + (int)sh > y1) y1 = stop + (int)sh;
|
|
||||||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
|
||||||
kid_lw[dp] = (uint16_t)(x1 - x0);
|
|
||||||
kid_lh[dp] = (uint16_t)(y1 - y0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
|
/* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
|
||||||
|
* fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
|
||||||
|
* тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
|
||||||
|
* нулевое окно = fore-проход не трогает экран, что и правильно. */
|
||||||
|
kid_fore_clip(gfx_get_draw_page() & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Вернуть окно fore-клипа на прямоугольник ПОСЛЕДНЕГО кадра Kid.
|
||||||
|
*
|
||||||
|
* Окно (pop_fore_set_clip) — одно на всех, и его ставит КАЖДЫЙ, кто рисует
|
||||||
|
* персонажа: kid_draw в конце — своё, pop_guard_draw в конце — своё. Пока
|
||||||
|
* страж рисовался строго ДО Кида, к моменту pop_fore_over_kid в окне
|
||||||
|
* оказывался Kid, и всё сходилось само. Как только порядок стал
|
||||||
|
* переменным (guard_over_kid в roomtest.c — порядок задаёт обход тайлов),
|
||||||
|
* при «страж поверх» окно оставалось СТРАЖЬИМ и fore-проход Кида отсекался
|
||||||
|
* целиком: Кид рисовался поверх передних столбов.
|
||||||
|
*
|
||||||
|
* Поэтому окно восстанавливается явно перед pop_fore_over_kid, а не
|
||||||
|
* держится «по счастливому порядку вызовов». */
|
||||||
|
void kid_fore_clip_restore(void)
|
||||||
|
{
|
||||||
|
kid_fore_clip(gfx_get_draw_page() & 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,29 +11,37 @@
|
|||||||
#define POP_KID_H
|
#define POP_KID_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include "pop_char.h"
|
||||||
|
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
|
||||||
|
|
||||||
/* Состояние персонажа (подмножество char_type, types.h). */
|
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
|
||||||
typedef struct {
|
extern pop_char_t Kid;
|
||||||
uint8_t frame;
|
|
||||||
uint8_t x, y;
|
|
||||||
int8_t direction; /* -1 влево, 0 вправо */
|
|
||||||
int8_t curr_col, curr_row;
|
|
||||||
uint8_t action;
|
|
||||||
int8_t fall_x, fall_y;
|
|
||||||
uint8_t repeat;
|
|
||||||
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
|
|
||||||
} kid_t;
|
|
||||||
|
|
||||||
extern kid_t Kid;
|
|
||||||
|
|
||||||
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
|
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
|
||||||
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
|
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
|
||||||
extern int8_t knock;
|
extern int8_t knock;
|
||||||
|
|
||||||
|
/* Порт next_level (seg000): END_LEVEL в play_seq инкрементит — «уровень
|
||||||
|
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
|
||||||
|
* там рестарт того же уровня; загрузка следующего — levels_plan.md. */
|
||||||
|
extern uint8_t pop_next_level;
|
||||||
|
|
||||||
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
||||||
* 0 — OK, -1 — ошибка. */
|
* 0 — OK, -1 — ошибка. */
|
||||||
int pop_kid_load(uint8_t npages);
|
int pop_kid_load(uint8_t npages);
|
||||||
|
|
||||||
|
/* Данные анимации (таблица кадров + seqtbl) — отдельная EMM-страница
|
||||||
|
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
|
||||||
|
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
|
||||||
|
* Страница маппится в W0 внутри play_seq, раз за тик. */
|
||||||
|
int pop_kid_data_load(const char *path);
|
||||||
|
|
||||||
|
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
|
||||||
|
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
|
||||||
|
* Маппит страницу сама; для горячего цикла есть cur_frame. */
|
||||||
|
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5);
|
||||||
|
void pop_kid_data_free(void);
|
||||||
|
|
||||||
/* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах
|
/* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах
|
||||||
* подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798).
|
* подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798).
|
||||||
* Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда
|
* Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда
|
||||||
@@ -41,6 +49,15 @@ int pop_kid_load(uint8_t npages);
|
|||||||
int pop_sword_load(void);
|
int pop_sword_load(void);
|
||||||
void pop_kid_free(void);
|
void pop_kid_free(void);
|
||||||
|
|
||||||
|
/* Нарисовать клинок chtab_0 поверх персонажа (порт add_sword_to_objtable,
|
||||||
|
* seg006:1798) — общая точка для Кида и стража: меч в оригинале ОДИН на
|
||||||
|
* всех. obj_x/obj_y — та же опорная точка, что у спрайта персонажа.
|
||||||
|
* Возвращает 1 и прямоугольник клинка (для heal), 0 — клинка нет.
|
||||||
|
* Страницу W0 оставляет замапленной: вызывающий делает gfx_w0_unmap. */
|
||||||
|
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
|
||||||
|
int obj_x, int obj_y,
|
||||||
|
int *rx, int *ry, uint16_t *rw, uint16_t *rh);
|
||||||
|
|
||||||
/* Инициализировать Kid: последовательность seq_id, клетка col/row,
|
/* Инициализировать Kid: последовательность seq_id, клетка col/row,
|
||||||
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */
|
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */
|
||||||
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
|
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
|
||||||
@@ -51,11 +68,46 @@ void kid_tick(void);
|
|||||||
|
|
||||||
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
|
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
|
||||||
* physics-шаг, как оригинал). */
|
* physics-шаг, как оригинал). */
|
||||||
void play_seq(void);
|
void play_seq(void); /* работает с Char — см. pop_kid_play ниже */
|
||||||
|
|
||||||
|
/* Активный персонаж (порт Char, seg006) и его загрузка/выгрузка.
|
||||||
|
* Интерпретатор последовательностей общий для Kid и стража: у них общая
|
||||||
|
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
|
||||||
|
extern pop_char_t Char;
|
||||||
|
/* Соперник активного персонажа (порт Opp): ставится вместе с Char через
|
||||||
|
* pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */
|
||||||
|
extern pop_char_t Opp;
|
||||||
|
void pop_loadkid(void);
|
||||||
|
void pop_savekid(void);
|
||||||
|
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
|
||||||
|
void pop_loadshad(void);
|
||||||
|
void pop_saveshad(void);
|
||||||
|
void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */
|
||||||
|
void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */
|
||||||
|
void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */
|
||||||
|
void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */
|
||||||
|
|
||||||
|
/* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной,
|
||||||
|
* 999 — разные комнаты (порт char_opp_dist, seg006:2135). */
|
||||||
|
int16_t pop_char_opp_dist(void);
|
||||||
|
|
||||||
|
/* Прокрутить последовательность персонажа целиком (load + play_seq +
|
||||||
|
* save). Звать вместо голого play_seq. */
|
||||||
|
void pop_char_set_seq(uint8_t seq_id); /* seq активного персонажа */
|
||||||
|
void pop_kid_play(void);
|
||||||
|
void pop_guard_play(void);
|
||||||
|
|
||||||
|
/* load_fram_det_col (seg006:0144) — перезагрузить cur_frame под АКТИВНОГО
|
||||||
|
* персонажа (Char) и пересчитать его колонку. Звать сразу после pop_loadkid/
|
||||||
|
* pop_loadshad и ДО диспетчера control(), как это делают play_kid_frame /
|
||||||
|
* play_guard_frame (seg000:1209/1246): cur_frame один на всех, и без этого
|
||||||
|
* геометрия одного персонажа считается по кадру другого. */
|
||||||
|
void pop_load_fram_det_col(void);
|
||||||
|
|
||||||
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
|
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
|
||||||
* таблицы во втором TU). */
|
* таблицы во втором TU). */
|
||||||
int8_t kid_cur_dx(void);
|
int8_t kid_cur_dx(void);
|
||||||
|
int8_t kid_cur_dy(void);
|
||||||
uint8_t kid_cur_flags(void);
|
uint8_t kid_cur_flags(void);
|
||||||
|
|
||||||
/* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая
|
/* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая
|
||||||
@@ -74,10 +126,19 @@ void pop_kid_set_render_dx(int dx);
|
|||||||
* теневой копии; запоминает прямоугольник для kid_heal). */
|
* теневой копии; запоминает прямоугольник для kid_heal). */
|
||||||
void kid_draw(void);
|
void kid_draw(void);
|
||||||
|
|
||||||
|
/* Вернуть окно fore-клипа на прямоугольник последнего кадра Kid. Звать
|
||||||
|
* ПЕРЕД pop_fore_over_kid, если между ним и kid_draw кто-то ещё рисовал
|
||||||
|
* персонажа (pop_guard_draw ставит окно по стражу). */
|
||||||
|
void kid_fore_clip_restore(void);
|
||||||
|
|
||||||
/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid —
|
/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid —
|
||||||
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
|
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
|
||||||
void kid_draw_splash(void);
|
void kid_draw_splash(void);
|
||||||
|
|
||||||
|
/* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP
|
||||||
|
* (id 216 полное деление / 217 пустое). */
|
||||||
|
void pop_kid_img_blit(uint8_t image, int x, int y);
|
||||||
|
|
||||||
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
|
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
|
||||||
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
|
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
|
||||||
void kid_heal(void);
|
void kid_heal(void);
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
extern void _gfx_w0_isr(void);
|
extern void _gfx_w0_isr(void);
|
||||||
|
|
||||||
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */
|
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */
|
||||||
|
/* Эталонная копия foretable В ТОЙ ЖЕ странице: страница 16 КБ, данных
|
||||||
|
* уровня 2.3 КБ — место даром. Нужна, потому что foretable теперь
|
||||||
|
* МУТАБЕЛЬНАЯ (pop_level_set_tile), а рестарт уровня в оригинале —
|
||||||
|
* это load_level() заново (play_level, seg003:57), то есть все плиты целы
|
||||||
|
* и зелья на месте. pop_level_reset_tiles восстанавливает из неё. */
|
||||||
|
#define LVL_PRISTINE_OFF 0x1000u
|
||||||
|
|
||||||
/* Смещения блоков blueprnt относительно начала данных (Table 6). */
|
/* Смещения блоков blueprnt относительно начала данных (Table 6). */
|
||||||
#define BP_FG 0u
|
#define BP_FG 0u
|
||||||
@@ -23,10 +29,22 @@ extern void _gfx_w0_isr(void);
|
|||||||
#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */
|
#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */
|
||||||
#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */
|
#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */
|
||||||
#define BP_LINKS 1952u
|
#define BP_LINKS 1952u
|
||||||
|
/* Стражи (level_type, types.h:228): по байту на комнату в каждом массиве. */
|
||||||
|
#define BP_GTILE 2119u
|
||||||
|
#define BP_GDIR 2143u
|
||||||
|
#define BP_GX 2167u
|
||||||
|
#define BP_GSEQLO 2191u
|
||||||
|
#define BP_GSKILL 2215u
|
||||||
|
#define BP_GSEQHI 2239u
|
||||||
|
#define BP_GCOLOR 2263u
|
||||||
#define BP_START 2112u
|
#define BP_START 2112u
|
||||||
#define ROOM_TILES 30u
|
#define ROOM_TILES 30u
|
||||||
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
|
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
|
||||||
|
|
||||||
|
/* fwd: инициализация живой копии стражей (тело ниже). */
|
||||||
|
static void gstate_init(void);
|
||||||
|
|
||||||
|
|
||||||
static uint8_t lvl_blk, lvl_page, lvl_ok;
|
static uint8_t lvl_blk, lvl_page, lvl_ok;
|
||||||
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
|
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
|
||||||
static uint8_t dl1[256], dl2[256];
|
static uint8_t dl1[256], dl2[256];
|
||||||
@@ -62,12 +80,16 @@ int pop_level_load(const char *path)
|
|||||||
dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
|
dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
|
||||||
dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
|
dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
|
||||||
}
|
}
|
||||||
|
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
|
||||||
|
for (i = 0; i < 24u * ROOM_TILES; i++)
|
||||||
|
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
|
||||||
}
|
}
|
||||||
sprinter_page_w3(saved_w3);
|
sprinter_page_w3(saved_w3);
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|
||||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||||
lvl_ok = 1;
|
lvl_ok = 1;
|
||||||
|
gstate_init(); /* живая копия состояния стражей — из уровня */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +98,52 @@ void pop_level_free(void)
|
|||||||
if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; }
|
if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Номер уровня и потабличные различия ---------------------------- */
|
||||||
|
|
||||||
|
uint8_t pop_current_level;
|
||||||
|
|
||||||
|
/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = демо).
|
||||||
|
* Держим целиком по 16 байт: короче и дешевле любых «оптимизаций», а
|
||||||
|
* расхождение с оригиналом сразу видно глазами. */
|
||||||
|
static const uint8_t tbl_entry_pose[16] =
|
||||||
|
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0 };
|
||||||
|
static const uint8_t tbl_guard_hp[16] =
|
||||||
|
{ 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0, 0 };
|
||||||
|
static const int8_t tbl_guard_type[16] =
|
||||||
|
{ 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1, -1 };
|
||||||
|
static const uint8_t tbl_level_type[16] =
|
||||||
|
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0 };
|
||||||
|
|
||||||
|
uint8_t pop_level_entry_pose(uint8_t n) { return n < 16 ? tbl_entry_pose[n] : 0; }
|
||||||
|
uint8_t pop_level_guard_hp(uint8_t n) { return n < 16 ? tbl_guard_hp[n] : 3; }
|
||||||
|
int8_t pop_level_guard_type(uint8_t n) { return n < 16 ? tbl_guard_type[n] : -1; }
|
||||||
|
uint8_t pop_level_tileset(uint8_t n) { return n < 16 ? tbl_level_type[n] : 0; }
|
||||||
|
|
||||||
|
int pop_level_load_num(uint8_t n)
|
||||||
|
{
|
||||||
|
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
||||||
|
* резидент W1 форматтер, которого здесь больше негде применить. */
|
||||||
|
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
|
||||||
|
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
|
||||||
|
uint8_t saved_blk = lvl_blk, saved_page = lvl_page, saved_ok = lvl_ok;
|
||||||
|
|
||||||
|
if (n > 15) return -1;
|
||||||
|
pri[12] = alt[8] = (char)('0' + n / 10);
|
||||||
|
pri[13] = alt[9] = (char)('0' + n % 10);
|
||||||
|
|
||||||
|
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
||||||
|
* pop_level_load сам аллоцирует страницу, и если файла нет — остаёмся
|
||||||
|
* на текущем уровне вместо падения в пустой уровень. */
|
||||||
|
lvl_ok = 0; /* чтобы pop_level_load не мешался */
|
||||||
|
if (pop_level_load(pri) != 0 && pop_level_load(alt) != 0) {
|
||||||
|
lvl_blk = saved_blk; lvl_page = saved_page; lvl_ok = saved_ok;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (saved_ok) mem_free_block(saved_blk);
|
||||||
|
pop_current_level = n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
|
/* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
|
||||||
* замаплена вызывающим). */
|
* замаплена вызывающим). */
|
||||||
static const uint8_t *room_fg_ptr(uint8_t r)
|
static const uint8_t *room_fg_ptr(uint8_t r)
|
||||||
@@ -142,12 +210,23 @@ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
|
|||||||
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
|
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* belowrow: верхний ряд (row0) комнаты снизу */
|
/* 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) {
|
if (down >= 1 && down <= 24) {
|
||||||
const uint8_t *df = room_fg_ptr(down);
|
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);
|
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 {
|
} else {
|
||||||
for (i = 0; i < 10; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */
|
for (i = 0; i < 11; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
@@ -171,6 +250,48 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col)
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* set_tile-запись в ЖИВУЮ foretable уровня (порт curr_room_tiles[…] = …).
|
||||||
|
* В оригинале уровень — обычная структура в ОЗУ, и do_pickup (seg006:1671),
|
||||||
|
* remove_loose (seg007:0EB8), loose_land (seg007:11E8) правят её насовсем.
|
||||||
|
* У нас уровень в EMM-странице — пишем прямо туда, поэтому изменение
|
||||||
|
* переживает выход из комнаты (BUG-LVLSTATE-1). Пишем БАЙТ ЦЕЛИКОМ:
|
||||||
|
* старшие биты старого тайла (напр. 0x20 «solid loose») к новому типу
|
||||||
|
* отношения не имеют. */
|
||||||
|
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
|
||||||
|
{
|
||||||
|
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return;
|
||||||
|
gfx_w0_map(lvl_page);
|
||||||
|
((uint8_t *)(LVL_DATA_OFF + BP_FG))[(uint16_t)(room - 1) * ROOM_TILES + tilepos]
|
||||||
|
= tile;
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть
|
||||||
|
* foretable в исходное состояние из эталонной копии. */
|
||||||
|
void pop_level_reset_tiles(void)
|
||||||
|
{
|
||||||
|
uint16_t i;
|
||||||
|
uint8_t *dst;
|
||||||
|
const uint8_t *src;
|
||||||
|
if (!lvl_ok) return;
|
||||||
|
gfx_w0_map(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];
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Рестарт уровня, часть вторая (тот же load_level + pos_guards, seg003:83):
|
||||||
|
* стражи — такая же часть данных уровня, как тайлы, и перезагрузка файла
|
||||||
|
* возвращает их всех. Убитый до смерти Кида страж после respawn снова жив
|
||||||
|
* и стоит на исходном тайле; забывается и запомненная поза трупа
|
||||||
|
* (guards_seq_hi = 0 -> «поднять стандартной стойкой»). */
|
||||||
|
void pop_level_reset_guards(void)
|
||||||
|
{
|
||||||
|
if (!lvl_ok) return;
|
||||||
|
gstate_init();
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; }
|
uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; }
|
||||||
uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; }
|
uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; }
|
||||||
void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }
|
void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }
|
||||||
@@ -234,6 +355,101 @@ static uint8_t start_byte(uint16_t off)
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
|
||||||
|
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
|
||||||
|
* колонки (pos_guards), поэтому guards_x из файла не читаем. */
|
||||||
|
/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ *
|
||||||
|
* В оригинале массивы стражей — часть структуры уровня в ОЗУ, и движок их
|
||||||
|
* ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие
|
||||||
|
* позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq. Благодаря
|
||||||
|
* этому, вернувшись, находишь труп там же, а не воскресшего стража.
|
||||||
|
* У нас уровень лежит в EMM-странице только на чтение, поэтому держим
|
||||||
|
* мутабельную копию в W2: 6 байт на комнату. */
|
||||||
|
#define GS_TILE 0
|
||||||
|
#define GS_DIR 1
|
||||||
|
#define GS_X 2
|
||||||
|
#define GS_SKILL 3
|
||||||
|
#define GS_SEQL 4
|
||||||
|
#define GS_SEQH 5
|
||||||
|
static uint8_t gstate[24 * 6];
|
||||||
|
|
||||||
|
static void gstate_init(void)
|
||||||
|
{
|
||||||
|
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
|
||||||
|
uint8_t r;
|
||||||
|
gfx_w0_map(lvl_page);
|
||||||
|
for (r = 0; r < 24; r++) {
|
||||||
|
uint8_t *g = 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];
|
||||||
|
/* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты. seq
|
||||||
|
* сохраняем ТОЛЬКО у мёртвого — у живого seq_hi = 0 означает «поднять
|
||||||
|
* стандартной стойкой», как в оригинале. */
|
||||||
|
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
|
||||||
|
uint8_t skill, uint16_t seq, uint8_t dead)
|
||||||
|
{
|
||||||
|
uint8_t *g;
|
||||||
|
if (room < 1 || room > 24) return;
|
||||||
|
g = gstate + (uint16_t)(room - 1) * 6;
|
||||||
|
g[GS_TILE] = tile;
|
||||||
|
g[GS_DIR] = (uint8_t)dir;
|
||||||
|
g[GS_X] = x;
|
||||||
|
g[GS_SKILL] = skill;
|
||||||
|
if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
|
||||||
|
else { g[GS_SEQH] = 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pop_guard_state_seq(uint8_t room)
|
||||||
|
{
|
||||||
|
const uint8_t *g;
|
||||||
|
if (room < 1 || room > 24) return 0;
|
||||||
|
g = gstate + (uint16_t)(room - 1) * 6;
|
||||||
|
if (g[GS_SEQH] == 0) return 0; /* живой — стандартная стойка */
|
||||||
|
return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
|
||||||
|
* центре тайла). */
|
||||||
|
uint8_t pop_guard_state_x(uint8_t room)
|
||||||
|
{
|
||||||
|
if (room < 1 || room > 24) return 0;
|
||||||
|
return gstate[(uint16_t)(room - 1) * 6 + GS_X];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||||
|
uint8_t *color, uint8_t *skill)
|
||||||
|
{
|
||||||
|
const uint8_t *g;
|
||||||
|
uint8_t t;
|
||||||
|
if (!lvl_ok || room < 1 || room > 24) return 0;
|
||||||
|
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
|
||||||
|
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
|
||||||
|
g = 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(lvl_page);
|
||||||
|
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
return (uint8_t)(t < 30);
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); }
|
uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); }
|
||||||
uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); }
|
uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); }
|
||||||
int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); }
|
int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); }
|
||||||
|
|||||||
@@ -26,6 +26,36 @@
|
|||||||
int pop_level_load(const char *path);
|
int pop_level_load(const char *path);
|
||||||
void pop_level_free(void);
|
void pop_level_free(void);
|
||||||
|
|
||||||
|
/* ---- Номер уровня и потабличные различия (levels_plan.md §1) -------- */
|
||||||
|
|
||||||
|
/* Порт current_level (seg000 load_lev_spr: current_level = next_level =
|
||||||
|
* level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают
|
||||||
|
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
|
||||||
|
extern uint8_t pop_current_level;
|
||||||
|
|
||||||
|
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
||||||
|
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
|
||||||
|
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
|
||||||
|
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
|
||||||
|
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
|
||||||
|
int pop_level_load_num(uint8_t n);
|
||||||
|
|
||||||
|
/* tbl_entry_pose (data.h:848): 1 — падение внутрь (ур. 1), 2 — вбегание
|
||||||
|
* (ур. 13), 0 — разворот на месте. Индекс — номер уровня. */
|
||||||
|
uint8_t pop_level_entry_pose(uint8_t n);
|
||||||
|
|
||||||
|
/* tbl_guard_hp (data.h:846) — база HP стража на уровне (get_guard_hp,
|
||||||
|
* seg002:0044: extrastrength[skill] + tbl_guard_hp[level]). */
|
||||||
|
uint8_t pop_level_guard_hp(uint8_t n);
|
||||||
|
|
||||||
|
/* tbl_guard_type (data.h:844): 0 обычный, 1 толстый, 2 скелет, 3 визирь,
|
||||||
|
* 4 тень, −1 — стражей на уровне НЕТ (уровни 14/15). */
|
||||||
|
int8_t pop_level_guard_type(uint8_t n);
|
||||||
|
|
||||||
|
/* tbl_level_type (data.h:840): 0 = dungeon, 1 = palace. Пока читается
|
||||||
|
* только для контроля — второй тайлсет это levels_plan.md §3. */
|
||||||
|
uint8_t pop_level_tileset(uint8_t n);
|
||||||
|
|
||||||
/* Извлечь комнату room (1..24) в массивы приложения (W2):
|
/* Извлечь комнату room (1..24) в массивы приложения (W2):
|
||||||
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
|
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
|
||||||
* lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
|
* lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
|
||||||
@@ -33,7 +63,8 @@ void pop_level_free(void);
|
|||||||
* для коллизии на колонке −2 (Kid стоит В шве);
|
* для коллизии на колонке −2 (Kid стоит В шве);
|
||||||
* rcol_fg[6] симметрично (col0 / col1);
|
* rcol_fg[6] симметрично (col0 / col1);
|
||||||
* нет комнаты → стена/0;
|
* нет комнаты → стена/0;
|
||||||
* below_fg[10] — верхний ряд (row0) комнаты СНИЗУ («стена
|
* below_fg[11] — [0..9] верхний ряд (row0) комнаты СНИЗУ,
|
||||||
|
* [10] тайл (0,9) комнаты СНИЗУ-СЛЕВА («стена
|
||||||
* вниз»); нет комнаты → стена.
|
* вниз»); нет комнаты → стена.
|
||||||
* 0 — OK, -1 — уровень не загружен / room вне диапазона. */
|
* 0 — OK, -1 — уровень не загружен / room вне диапазона. */
|
||||||
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
|
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
|
||||||
@@ -49,6 +80,20 @@ uint8_t pop_room_link(uint8_t room, uint8_t side);
|
|||||||
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
|
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
|
||||||
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
|
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
|
||||||
|
|
||||||
|
/* Записать тайл в ЖИВУЮ foretable уровня (порт curr_room_tiles[tp] = tile:
|
||||||
|
* do_pickup / remove_loose / loose_land). Изменение персистентно — уровень
|
||||||
|
* лежит в ОЗУ-странице, а не в ПЗУ, — то есть переживает выход из комнаты,
|
||||||
|
* как в оригинале. Отменяется только pop_level_reset_tiles. */
|
||||||
|
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile);
|
||||||
|
|
||||||
|
/* Рестарт уровня: вернуть ВСЕ тайлы в исходное (порт load_level, который
|
||||||
|
* play_level зовёт на каждой итерации — в т.ч. после смерти Кида). */
|
||||||
|
void pop_level_reset_tiles(void);
|
||||||
|
|
||||||
|
/* Рестарт уровня: вернуть ВСЕХ стражей (тот же load_level + pos_guards).
|
||||||
|
* Убитый страж после respawn снова жив — как в оригинале. */
|
||||||
|
void pop_level_reset_guards(void);
|
||||||
|
|
||||||
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
|
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
|
||||||
* Между begin/end — только pop_level_tile_raw, без рисования. */
|
* Между begin/end — только pop_level_tile_raw, без рисования. */
|
||||||
void pop_level_access_begin(void);
|
void pop_level_access_begin(void);
|
||||||
@@ -73,6 +118,23 @@ void pop_doorlink2_set(uint8_t i, uint8_t v);
|
|||||||
int8_t pop_room_col_landing(uint8_t room, uint8_t col);
|
int8_t pop_room_col_landing(uint8_t room, uint8_t col);
|
||||||
|
|
||||||
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
|
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
|
||||||
|
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
|
||||||
|
* Порт enter_guard (seg002:0112). */
|
||||||
|
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
|
||||||
|
* копии массивов уровня. dead != 0 — сохранить и последовательность, тогда
|
||||||
|
* при возврате в комнату страж поднимется трупом там же, где лёг.
|
||||||
|
* tile == 30 (или больше) — «стража в комнате больше нет». */
|
||||||
|
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
|
||||||
|
uint8_t skill, uint16_t seq, uint8_t dead);
|
||||||
|
|
||||||
|
/* Последовательность запомненного стража (0 — поднимать стандартной
|
||||||
|
* стойкой, как у живого). */
|
||||||
|
uint16_t pop_guard_state_seq(uint8_t room);
|
||||||
|
uint8_t pop_guard_state_x(uint8_t room);
|
||||||
|
|
||||||
|
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||||
|
uint8_t *color, uint8_t *skill);
|
||||||
|
|
||||||
uint8_t pop_level_start_room(void);
|
uint8_t pop_level_start_room(void);
|
||||||
uint8_t pop_level_start_pos(void);
|
uint8_t pop_level_start_pos(void);
|
||||||
int8_t pop_level_start_dir(void);
|
int8_t pop_level_start_dir(void);
|
||||||
|
|||||||
@@ -17,25 +17,45 @@
|
|||||||
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
||||||
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
|
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
|
||||||
* что и в pop_room_draw, чтобы отрисовка видела изменения. */
|
* что и в pop_room_draw, чтобы отрисовка видела изменения. */
|
||||||
void pop_map_set(uint8_t *fg);
|
void pop_map_set(uint8_t *fg) __banked;
|
||||||
|
|
||||||
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
|
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
|
||||||
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
|
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
|
||||||
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
|
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
|
||||||
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
|
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
|
||||||
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
|
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
|
||||||
const uint8_t *lcol_fg, const uint8_t *rcol_fg);
|
const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked;
|
||||||
|
|
||||||
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
|
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
|
||||||
* Звать в enter_room. */
|
* Звать в enter_room. */
|
||||||
void pop_map_set_room(uint8_t room);
|
void pop_map_set_room(uint8_t room) __banked;
|
||||||
|
|
||||||
|
/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт
|
||||||
|
* get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража
|
||||||
|
* (guards.c): он идёт по ряду между Кидом и стражем. */
|
||||||
|
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
|
||||||
|
|
||||||
|
/* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для
|
||||||
|
* pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */
|
||||||
|
void pop_determine_col(void) __banked;
|
||||||
|
|
||||||
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
|
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
|
||||||
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
|
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
|
||||||
* респавн). */
|
* респавн). */
|
||||||
extern uint8_t pop_kid_dead;
|
extern uint8_t pop_kid_dead;
|
||||||
extern uint8_t hitp_curr;
|
extern uint8_t hitp_curr;
|
||||||
void pop_kid_hp_reset(void);
|
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
|
||||||
|
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
|
||||||
|
void pop_kid_hp_reset(void) __banked;
|
||||||
|
|
||||||
|
/* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт
|
||||||
|
* уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень
|
||||||
|
* подтягивает его к hitp_max (play_level_2, seg003:410) — так «жизнь» из
|
||||||
|
* большой склянки переезжает на следующий уровень. Правит главный цикл. */
|
||||||
|
extern uint8_t hitp_beg_lev;
|
||||||
|
#define POP_START_HITP 3 /* custom->start_hitp */
|
||||||
|
#define POP_MAX_HITP 10 /* custom->max_hitp_allowed */
|
||||||
|
void pop_kid_hp_newgame(void) __banked; /* начало ИГРЫ: hitp_beg_lev = 3 */
|
||||||
|
|
||||||
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
|
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
|
||||||
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
|
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
|
||||||
@@ -46,8 +66,10 @@ extern uint8_t pop_item_taken;
|
|||||||
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
|
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
|
||||||
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
|
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
|
||||||
extern uint8_t pop_flash_time;
|
extern uint8_t pop_flash_time;
|
||||||
uint8_t pop_get_item_action(void); /* 0 нет / 1 присесть / 2 поднять меч */
|
/* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
|
||||||
void pop_proc_get_object(void); /* эффект — из play_seq (опкод GET_ITEM) */
|
extern uint8_t pop_flash_red;
|
||||||
|
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */
|
||||||
|
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
|
||||||
|
|
||||||
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
|
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
|
||||||
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
|
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
|
||||||
@@ -66,8 +88,8 @@ extern uint8_t pop_leave_timer;
|
|||||||
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
|
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
|
||||||
* рядом с kid-отрисовкой. */
|
* рядом с kid-отрисовкой. */
|
||||||
extern uint8_t pop_loose_modif[30];
|
extern uint8_t pop_loose_modif[30];
|
||||||
void pop_loose_tick(void);
|
void pop_loose_tick(void) __banked;
|
||||||
void pop_loose_reset(void); /* сброс loose-состояния при смене комнаты */
|
void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
|
||||||
|
|
||||||
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
|
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
|
||||||
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
|
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
|
||||||
@@ -81,7 +103,7 @@ extern uint8_t pop_loose_fell;
|
|||||||
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
|
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
|
||||||
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
|
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
|
||||||
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
|
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
|
||||||
void pop_map_set_above(uint8_t *above_row2_fg);
|
void pop_map_set_above(uint8_t *above_row2_fg) __banked;
|
||||||
extern uint8_t pop_ceil_modif[10];
|
extern uint8_t pop_ceil_modif[10];
|
||||||
extern uint8_t pop_ceil_fell;
|
extern uint8_t pop_ceil_fell;
|
||||||
extern uint8_t pop_debris_at;
|
extern uint8_t pop_debris_at;
|
||||||
@@ -92,7 +114,7 @@ extern uint8_t pop_kid_hurt;
|
|||||||
|
|
||||||
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
|
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
|
||||||
* приземления/удара. Зовётся из pop_phys_tick. */
|
* приземления/удара. Зовётся из pop_phys_tick. */
|
||||||
void pop_check_knock(void);
|
void pop_check_knock(void) __banked;
|
||||||
|
|
||||||
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
|
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
|
||||||
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
|
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
|
||||||
@@ -104,14 +126,18 @@ extern uint8_t pop_fell_out;
|
|||||||
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
|
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
|
||||||
* determine_col + check_action (триггер падения / do_fall / приземление).
|
* determine_col + check_action (триггер падения / do_fall / приземление).
|
||||||
* Порядок как в оригинале (seg000 play_kid_frame). */
|
* Порядок как в оригинале (seg000 play_kid_frame). */
|
||||||
void pop_phys_tick(void);
|
void pop_phys_tick(void) __banked;
|
||||||
|
|
||||||
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
|
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
|
||||||
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
|
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
|
||||||
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
|
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
|
||||||
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
|
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
|
||||||
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
|
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
|
||||||
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h);
|
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
|
||||||
|
|
||||||
|
/* Парная половина clip_char: ЭКРАННЫЙ X, правее которого спрайт не рисуется
|
||||||
|
* (правый косяк двери уровня в кадрах подъёма 224..228). 0 = обрезки нет. */
|
||||||
|
int pop_clip_char_right(void) __banked;
|
||||||
|
|
||||||
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
|
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
|
||||||
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
|
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
|
||||||
@@ -120,26 +146,39 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h);
|
|||||||
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
|
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
|
||||||
|
|
||||||
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
|
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
|
||||||
int pop_edge_distance(void);
|
int pop_edge_distance(void) __banked;
|
||||||
uint8_t pop_edge_type(void);
|
uint8_t pop_edge_type(void) __banked;
|
||||||
|
|
||||||
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
|
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
|
||||||
* позиции (передний край у стены, симметрично) и возвращает 1. Для
|
* позиции (передний край у стены, симметрично) и возвращает 1. Для
|
||||||
* control_running (чистый стоп у стены). */
|
* control_running (чистый стоп у стены). */
|
||||||
int pop_wall_ahead(void);
|
int pop_wall_ahead(void) __banked;
|
||||||
|
|
||||||
|
/* Разбег-прыжок: выравнивание по кромке пола (run_jump, seg005:0AA8).
|
||||||
|
* Смотрит на 1–2 тайла вперёд; если там провал/пика — подгоняет Kid.x под
|
||||||
|
* толчок ровно с кромки. 1 = прыгать, 0 = ОТМЕНИТЬ прыжок (Кид не в той
|
||||||
|
* фазе относительно кромки; control_up гасить НЕЛЬЗЯ — попытка повторится
|
||||||
|
* на следующем кадре). Без этого провал в три тайла непроходим. */
|
||||||
|
uint8_t pop_run_jump_align(void) __banked;
|
||||||
|
|
||||||
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
|
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
|
||||||
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
|
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
|
||||||
* check_jump_up/grab_up (K4.4). control_up гасит caller. */
|
* check_jump_up/grab_up (K4.4). control_up гасит caller. */
|
||||||
uint8_t pop_jump_up_seq(void);
|
uint8_t pop_jump_up_seq(void) __banked;
|
||||||
|
|
||||||
/* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
|
/* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
|
||||||
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
|
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
|
||||||
* pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */
|
* pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */
|
||||||
uint8_t pop_hang_grab_ready(void); /* grab_timer истёк — можно climb-up */
|
uint8_t pop_hang_grab_ready(void) __banked; /* grab_timer истёк — можно climb-up */
|
||||||
uint8_t pop_hang_climb_up_seq(void); /* seq_10 / seq_73 (подтягивание) */
|
uint8_t pop_hang_climb_up_seq(void) __banked; /* seq_10 / seq_73 (подтягивание) */
|
||||||
int8_t pop_hang_wall(void); /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
|
int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
|
||||||
uint8_t pop_hang_fall_seq(void); /* seq_23 / seq_11 (сорваться), +Kid.x */
|
uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
|
||||||
uint8_t pop_down_action(void); /* 1=обработано(спуск/отступ), 0=присед */
|
uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
|
||||||
|
|
||||||
|
/* Дверь уровня рядом и ОТКРЫТА ПОЛНОСТЬЮ? → поставить Кида в проём (x +
|
||||||
|
* разворот влево, go_up_leveldoor) и вернуть 1; иначе 0, Кид не тронут.
|
||||||
|
* Ветка двери уровня из up_pressed (seg005:0482). Последовательность
|
||||||
|
* seq_70 ставит вызывающий (pop_ctrl) — тайлы наши, управление его. */
|
||||||
|
uint8_t pop_leveldoor_enter(void) __banked;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* pop_redraw.c — пометки перерисовки тайлов и их разбор. См. pop_redraw.h.
|
||||||
|
*
|
||||||
|
* Живёт в W1/W2 (НЕ в резиденте W3): пометки ставит логика, которая должна
|
||||||
|
* оставаться достижимой из будущих __banked-модулей. Сам разбор зовёт
|
||||||
|
* pop_bg (резидент W3) — это законно, потому что разбор дёргает только
|
||||||
|
* главный цикл.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "pop_redraw.h"
|
||||||
|
#include "pop_bg.h"
|
||||||
|
|
||||||
|
#define NTILES 30
|
||||||
|
#define NCOLS 10
|
||||||
|
|
||||||
|
static uint8_t rd_kind[NTILES], rd_cnt[NTILES];
|
||||||
|
static uint8_t rda_kind[NCOLS], rda_cnt[NCOLS];
|
||||||
|
/* Сколько всего непустых пометок: без него проход по 40 ячейкам крутился бы
|
||||||
|
* КАЖДЫЙ кадр впустую (замер 2026-07-27 на прежнем rest-цикле: ~12 % кадра). */
|
||||||
|
static uint8_t rd_pending;
|
||||||
|
|
||||||
|
void pop_redraw_reset(void)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < NTILES; i++) { rd_kind[i] = 0; rd_cnt[i] = 0; }
|
||||||
|
for (i = 0; i < NCOLS; i++) { rda_kind[i] = 0; rda_cnt[i] = 0; }
|
||||||
|
rd_pending = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
|
||||||
|
{
|
||||||
|
if (tilepos >= NTILES) return;
|
||||||
|
if (!rd_cnt[tilepos]) rd_pending++;
|
||||||
|
rd_kind[tilepos] = kind;
|
||||||
|
if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
|
||||||
|
{
|
||||||
|
if (col >= NCOLS) return;
|
||||||
|
if (!rda_cnt[col]) rd_pending++;
|
||||||
|
rda_kind[col] = kind;
|
||||||
|
if (pages > rda_cnt[col]) rda_cnt[col] = pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* redraw_needed (seg008:0178): пройти пометки и нарисовать. row/col ведём
|
||||||
|
* бегущими счётчиками, а не через i/10 и i%10 — у Z80 нет деления, и SDCC
|
||||||
|
* выносит __divuchar/__moduchar в вершину цикла как общее подвыражение
|
||||||
|
* аргументов (30*2 делений за кадр даже когда рисовать нечего). */
|
||||||
|
void pop_redraw_needed(void)
|
||||||
|
{
|
||||||
|
uint8_t i, row = 0, col = 0, left = 0;
|
||||||
|
if (!rd_pending) return;
|
||||||
|
for (i = 0; i < NTILES; i++) {
|
||||||
|
if (rd_cnt[i]) {
|
||||||
|
switch (rd_kind[i]) {
|
||||||
|
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
|
||||||
|
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
|
||||||
|
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break;
|
||||||
|
case POP_RD_FLOOR: pop_floor_bake(row, col); break;
|
||||||
|
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
|
||||||
|
case POP_RD_GATE: pop_gate_redraw(row, col); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if (--rd_cnt[i]) left++;
|
||||||
|
else rd_kind[i] = 0;
|
||||||
|
}
|
||||||
|
if (++col == 10) { col = 0; row++; }
|
||||||
|
}
|
||||||
|
for (i = 0; i < NCOLS; i++) {
|
||||||
|
if (rda_cnt[i]) {
|
||||||
|
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
|
||||||
|
else pop_ceil_bake_empty(i);
|
||||||
|
if (--rda_cnt[i]) left++;
|
||||||
|
else rda_kind[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rd_pending = left;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* pop_redraw.h — пометки «перерисовать тайл» и их разбор.
|
||||||
|
*
|
||||||
|
* Порт архитектуры оригинала: логика анимации НИЧЕГО не рисует, она ставит
|
||||||
|
* флаг (`set_redraw_full`/`set_wipe`/`redraw_20h`/`redraw_21h`, seg007), а
|
||||||
|
* отрисовка идёт отдельным проходом (`redraw_needed`, seg008:0178). У нас
|
||||||
|
* так же: pop_map/pop_trob зовут pop_set_redraw*, а pop_redraw_needed()
|
||||||
|
* (главный цикл) разбирает пометки и дёргает pop_bg.
|
||||||
|
*
|
||||||
|
* ЗАЧЕМ (кроме сходства с SDLPoP): из __banked-кода резидентная страница W3
|
||||||
|
* недостижима — ни напрямую, ни транзитивно (docs/layout_plan_v2.md §2 R2).
|
||||||
|
* Пока pop_map звал pop_bg, его нельзя было использовать из банка стражей.
|
||||||
|
* Пометки живут в W1/W2 и зовутся откуда угодно.
|
||||||
|
*
|
||||||
|
* ОТЛИЧИЕ ОТ ОРИГИНАЛА: у нас не один экран с dirty-rect, а две страницы
|
||||||
|
* дабл-буфера со своей ОЗУ-копией фона, поэтому счётчик пометки — это ЧИСЛО
|
||||||
|
* СТРАНИЦ (обычно 2), а вид перерисовки хранится явно: heal+рисовать поверх
|
||||||
|
* (транзиент) или запечь новый фон. В оригинале вид кодируется тем, в какой
|
||||||
|
* из таблиц redraw_frames_* стоит флаг.
|
||||||
|
*/
|
||||||
|
#ifndef POP_REDRAW_H
|
||||||
|
#define POP_REDRAW_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Виды перерисовки тайла (row*10+col). */
|
||||||
|
#define POP_RD_SPIKE 1 /* пики: heal + оба тайла (redraw_21h) */
|
||||||
|
#define POP_RD_LOOSE 2 /* loose: дрожащий кадр / кадр покоя */
|
||||||
|
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
|
||||||
|
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
|
||||||
|
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
|
||||||
|
#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */
|
||||||
|
|
||||||
|
/* Виды перерисовки полосы у потолка (по колонке). */
|
||||||
|
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
|
||||||
|
#define POP_RDA_CEIL_GONE 2 /* плита-потолок провалилась: колодец */
|
||||||
|
|
||||||
|
/* Пометить тайл/колонку: kind — вид, pages — на скольких страницах
|
||||||
|
* дабл-буфера перерисовать (1 = только текущая, 2 = обе). Повторная
|
||||||
|
* пометка перекрывает вид и берёт максимум счётчика. */
|
||||||
|
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages);
|
||||||
|
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages);
|
||||||
|
|
||||||
|
/* Разобрать пометки: нарисовать всё помеченное на ТЕКУЩЕЙ (back) странице и
|
||||||
|
* уменьшить счётчики. Звать из главного цикла в слое ФОНА — до kid_draw. */
|
||||||
|
void pop_redraw_needed(void);
|
||||||
|
|
||||||
|
/* Сбросить все пометки (смена комнаты/уровня). */
|
||||||
|
void pop_redraw_reset(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* pop_state.c — данные, которые читают ОБЕ стороны банковой границы.
|
||||||
|
*
|
||||||
|
* Зачем отдельный модуль: sprinter-cc собирает банковый модуль с
|
||||||
|
* --dataseg BANKn, поэтому ВСЯ его писучая память (глобалы, file-static,
|
||||||
|
* статики внутри функций) линкуется В СТРАНИЦУ БАНКА и снаружи не читается
|
||||||
|
* — проверено на .map (2026-07-30: глобал банкового модуля лёг по
|
||||||
|
* 0x0001C000, а не в _DATA). Значит любую переменную, к которой нужен
|
||||||
|
* доступ и из банка, и снаружи, надо держать в НЕбанковом модуле — здесь.
|
||||||
|
*
|
||||||
|
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
|
||||||
|
*/
|
||||||
|
#include "pop_state.h"
|
||||||
|
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
|
||||||
|
|
||||||
|
uint8_t pop_loose_landed;
|
||||||
|
|
||||||
|
/* ---- Шина управления (seg006 globals) ------------------------------ *
|
||||||
|
* Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка —
|
||||||
|
* pop_ctrl (банк 5) пишет с клавиатуры, guards.c (банк 1) подаёт
|
||||||
|
* синтетический ввод ИИ (autocontrol_*, seg002), pop_map (банк 3) читает
|
||||||
|
* через pop_ctrl_shift_held. Сегодня оно уцелело бы и в pop_ctrl.c
|
||||||
|
* (собираем без --bank-data, данные банков остаются в _DATA), но это
|
||||||
|
* флаг сборки: включат его — и шина уедет в страницу банка 5.
|
||||||
|
*
|
||||||
|
* control_x/y/shift — «сырое» состояние осей ЭТОГО кадра (абсолютно:
|
||||||
|
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
|
||||||
|
* автомат held/released/ignore (авто-повтор). */
|
||||||
|
int8_t control_x, control_y, control_shift;
|
||||||
|
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
|
||||||
|
|
||||||
|
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
|
||||||
|
* но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */
|
||||||
|
uint8_t pop_immortal;
|
||||||
|
|
||||||
|
/* Габариты последней нарисованной двери уровня (порт leveldoor_right /
|
||||||
|
* leveldoor_ybottom): пишет draw_leveldoor из банка pop_bg, читает
|
||||||
|
* clip_char из pop_map. См. pop_state.h. */
|
||||||
|
int pop_leveldoor_right;
|
||||||
|
int pop_leveldoor_ybottom;
|
||||||
|
|
||||||
|
/* ---- Отладочная «пустышка» для брейкпоинтов из БАНКОВ ---------------- *
|
||||||
|
* Зачем. PC-брейкпоинт видит только логический адрес, а 0xC000+ — это
|
||||||
|
* окно, куда мапятся ВСЕ банки: точка на адресе банковой функции ловит
|
||||||
|
* заодно чужой код, случайно легший по тому же смещению (проверено:
|
||||||
|
* точка на start_fall из банка 3 срабатывала на каждом кадре — попадала
|
||||||
|
* в pop_bg из банка 2). Условные брейкпоинты этот отладчик MAME не
|
||||||
|
* поддерживает («error in assignment expression» на `==`).
|
||||||
|
*
|
||||||
|
* Приём (идея пользователя, 2026-08-04): позвать ЭТУ функцию ровно из
|
||||||
|
* того места банкового кода, которое отлаживаем, и поставить брейкпоинт
|
||||||
|
* на неё — она резидентна в W1, её адрес однозначен. По возврату из неё
|
||||||
|
* читаем что нужно. Условие «когда именно ловить» пишется обычным `if`
|
||||||
|
* в C — это гибче любых выражений отладчика.
|
||||||
|
*
|
||||||
|
* Счётчик нужен, чтобы вызов не выкинул оптимизатор; заодно видно, сколько
|
||||||
|
* раз точка прошла, если ловим не останавливаясь. */
|
||||||
|
uint8_t pop_dbg_hits;
|
||||||
|
|
||||||
|
void pop_dbg_trap(void) { pop_dbg_hits++; }
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* pop_state.h — состояние, общее для банков и W1/W2. См. pop_state.c.
|
||||||
|
*
|
||||||
|
* Правило: если переменную читают/пишут по обе стороны банковой границы —
|
||||||
|
* её место здесь, а НЕ в банковом модуле (там она уедет в страницу банка).
|
||||||
|
*/
|
||||||
|
#ifndef POP_STATE_H
|
||||||
|
#define POP_STATE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
|
||||||
|
* tilepos+1 тайла, на который он лёг. Ставит отрисовка mob (банк),
|
||||||
|
* разбирает логика loose-полов (pop_map, W1/W2). */
|
||||||
|
extern uint8_t pop_loose_landed;
|
||||||
|
|
||||||
|
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
|
||||||
|
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
|
||||||
|
extern uint8_t pop_immortal;
|
||||||
|
|
||||||
|
/* Габариты ПОСЛЕДНЕЙ нарисованной двери уровня — порт глобалов
|
||||||
|
* leveldoor_right / leveldoor_ybottom (seg008 draw_leveldoor). Их ставит
|
||||||
|
* отрисовка (банк pop_bg), а читает clip_char (pop_map, W1/W2): кадры
|
||||||
|
* подъёма по лестнице (224..228) обрезаются по правому косяку проёма и по
|
||||||
|
* низу поднятой створки. Дверь рисуется ПОД персонажем, поэтому перекрыть
|
||||||
|
* его не может — оригинал именно режет спрайт.
|
||||||
|
* right — экранный X правой кромки; ybottom — room-space Y (без POP_YOFF).
|
||||||
|
* 0 = двери в отрисованной комнате нет. */
|
||||||
|
extern int pop_leveldoor_right;
|
||||||
|
extern int pop_leveldoor_ybottom;
|
||||||
|
|
||||||
|
/* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c.
|
||||||
|
* Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на
|
||||||
|
* _pop_dbg_trap (резидент W1, адрес однозначен). */
|
||||||
|
extern uint8_t pop_dbg_hits;
|
||||||
|
void pop_dbg_trap(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -5,7 +5,16 @@
|
|||||||
*/
|
*/
|
||||||
#include "pop_trob.h"
|
#include "pop_trob.h"
|
||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "pop_bg.h" /* pop_spike_redraw */
|
#include "pop_geom.h"
|
||||||
|
#include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */
|
||||||
|
/* pop_bg нужен ТОЛЬКО для пламени факела и пузырька зелья: это не тайловая
|
||||||
|
* перерисовка, а покадровый оверлей поверх фона (в оригинале — свои
|
||||||
|
* add_backtable/add_foretable внутри draw_tile). Всё остальное идёт через
|
||||||
|
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
|
||||||
|
* нельзя звать из __banked-кода. */
|
||||||
|
#include "pop_bg.h"
|
||||||
|
#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */
|
||||||
|
#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */
|
||||||
|
|
||||||
/* Коды тайлов (fg & 0x1F), которые анимируются. */
|
/* Коды тайлов (fg & 0x1F), которые анимируются. */
|
||||||
#define TILE_SPIKE 0x02
|
#define TILE_SPIKE 0x02
|
||||||
@@ -14,6 +23,7 @@
|
|||||||
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
|
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
|
||||||
#define TILE_OPENER 0x0F
|
#define TILE_OPENER 0x0F
|
||||||
#define TILE_POTION 0x0A
|
#define TILE_POTION 0x0A
|
||||||
|
#define TILE_LOOSE 0x0B /* проваливающийся пол (фаза тряски в modif) */
|
||||||
#define TILE_SWORD 0x16 /* меч-предмет на полу */
|
#define TILE_SWORD 0x16 /* меч-предмет на полу */
|
||||||
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
|
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
|
||||||
#define TILE_TORCH 0x13
|
#define TILE_TORCH 0x13
|
||||||
@@ -30,11 +40,6 @@ static trob_t trobs[TROBS_MAX];
|
|||||||
static uint8_t trobs_count;
|
static uint8_t trobs_count;
|
||||||
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
|
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
|
||||||
|
|
||||||
/* Отложенная перерисовка «покоя» законченных пик/кнопок текущей комнаты
|
|
||||||
* (2 кадра = обе страницы дабл-буфера, как loose_rest). */
|
|
||||||
static uint8_t spike_rest[POP_ROOMTILES];
|
|
||||||
static uint8_t button_rest[POP_ROOMTILES];
|
|
||||||
|
|
||||||
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
|
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
|
||||||
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
|
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
|
||||||
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
|
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
|
||||||
@@ -43,51 +48,53 @@ static uint8_t button_rest[POP_ROOMTILES];
|
|||||||
* расхождение было именно в безусловном редрое). */
|
* расхождение было именно в безусловном редрое). */
|
||||||
static uint8_t trob_drawn[POP_ROOMTILES];
|
static uint8_t trob_drawn[POP_ROOMTILES];
|
||||||
|
|
||||||
/* Сколько тайлов ждут отложенного редроя (spike_rest/button_rest). Без
|
|
||||||
* него цикл по 30 тайлам крутился КАЖДЫЙ кадр впустую: замер в MAME
|
|
||||||
* (2026-07-27) — служебная часть pop_process_trobs ~50 000 тактов при
|
|
||||||
* кадре 430 080, то есть ~12 % кадра на пустой проход. */
|
|
||||||
static uint8_t rest_pending;
|
|
||||||
|
|
||||||
/* Дверь уровня: отложенный редрой ВТОРОЙ страницы дабл-буфера. Створка
|
|
||||||
* анимируется покадрово в back-страницу, поэтому последний кадр анимации
|
|
||||||
* попадает только на одну из двух страниц — вторая застревает на шаг раньше
|
|
||||||
* и дверь «дрожит» через кадр. ldoor_pos = tilepos ПРАВОЙ половины. */
|
|
||||||
static uint8_t ldoor_rest, ldoor_pos;
|
|
||||||
|
|
||||||
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
|
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
|
||||||
* выбора следующего кадра пламени. */
|
* выбора следующего кадра пламени. */
|
||||||
static unsigned long trob_seed;
|
static pop_rnd_t trob_seed;
|
||||||
|
|
||||||
static uint16_t trob_prandom(uint16_t maxv)
|
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
|
||||||
{
|
|
||||||
trob_seed = trob_seed * 214013UL + 2531011UL;
|
|
||||||
return (uint16_t)((uint16_t)(trob_seed >> 16) % (uint16_t)(maxv + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void pop_trob_reset(void)
|
void pop_trob_reset(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
|
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
|
||||||
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; }
|
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||||
rest_pending = 0;
|
pop_redraw_reset();
|
||||||
ldoor_rest = 0;
|
|
||||||
trobs_count = 0;
|
trobs_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t *pop_trob_modif(uint8_t room)
|
uint8_t *pop_trob_modif(uint8_t room) __banked
|
||||||
{
|
{
|
||||||
if (room < 1 || room > POP_NROOMS) return 0;
|
if (room < 1 || room > POP_NROOMS) return 0;
|
||||||
if (!room_seen[room - 1]) {
|
if (!room_seen[room - 1]) {
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
pop_level_room_bg(room, room_modif[room - 1]);
|
pop_level_room_bg(room, room_modif[room - 1]);
|
||||||
/* seg009 load_level: у ЗЕЛЬЯ модификатор сдвигается влево на 3 —
|
/* load_alter_mod (seg008:198E): модификатор В ФАЙЛЕ уровня и
|
||||||
* старшие биты = тип зелья, младшие 3 = фаза пузырька. */
|
* модификатор В РАНТАЙМЕ — разные величины, оригинал переводит их
|
||||||
|
* при загрузке. Порт того, что нас касается:
|
||||||
|
*
|
||||||
|
* ворота 1 = «открыты» (Table 8 спецификации DAT) -> 188, то есть
|
||||||
|
* рабочая высота подъёма; ЛЮБОЕ другое значение -> 0
|
||||||
|
* (закрыты). Без этого перевода ворота, которые в
|
||||||
|
* уровне открыты, стартуют закрытыми, и кнопка-closer
|
||||||
|
* рядом с ними теряет смысл (ур. 2, комн. 13).
|
||||||
|
* loose -> 0: фаза тряски всегда начинается с нуля.
|
||||||
|
* зелье -> <<3: старшие биты = тип, младшие 3 = фаза пузырька.
|
||||||
|
*
|
||||||
|
* Ветка стен (blue-line/связи кладки) НЕ портируется намеренно: у
|
||||||
|
* нас pop_bg считает связи по типам СОСЕДЕЙ прямо при отрисовке
|
||||||
|
* (wall_modifier), сохранённый модификатор стены не читается вовсе. */
|
||||||
pop_level_access_begin();
|
pop_level_access_begin();
|
||||||
for (i = 0; i < POP_ROOMTILES; i++)
|
for (i = 0; i < POP_ROOMTILES; i++) {
|
||||||
if ((pop_level_tile_raw(room, i) & 0x1F) == TILE_POTION)
|
uint8_t *m = &room_modif[room - 1][i];
|
||||||
room_modif[room - 1][i] = (uint8_t)(room_modif[room - 1][i] << 3);
|
switch (pop_level_tile_raw(room, i) & 0x1F) {
|
||||||
|
case TILE_GATE: *m = (uint8_t)((*m == 1) ? 188 : 0); break;
|
||||||
|
case TILE_LOOSE: *m = 0; break;
|
||||||
|
case TILE_POTION: *m = (uint8_t)(*m << 3); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
pop_level_access_end();
|
pop_level_access_end();
|
||||||
room_seen[room - 1] = 1;
|
room_seen[room - 1] = 1;
|
||||||
}
|
}
|
||||||
@@ -102,7 +109,7 @@ static int find_trob(uint8_t room, uint8_t tilepos)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type)
|
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
|
||||||
{
|
{
|
||||||
int f = find_trob(room, tilepos);
|
int f = find_trob(room, tilepos);
|
||||||
if (f >= 0) { trobs[f].type = type; return; }
|
if (f >= 0) { trobs[f].type = type; return; }
|
||||||
@@ -113,19 +120,18 @@ void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type)
|
|||||||
trobs_count++;
|
trobs_count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop_trob_room_changed(void)
|
void pop_trob_room_changed(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; }
|
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||||
rest_pending = 0;
|
pop_redraw_reset();
|
||||||
ldoor_rest = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
|
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
|
||||||
* тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch,
|
* тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch,
|
||||||
* seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька
|
* seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька
|
||||||
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
|
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
|
||||||
void pop_trob_anim_room(uint8_t room)
|
void pop_trob_anim_room(uint8_t room) __banked
|
||||||
{
|
{
|
||||||
uint8_t *mod = pop_trob_modif(room);
|
uint8_t *mod = pop_trob_modif(room);
|
||||||
uint8_t i, code;
|
uint8_t i, code;
|
||||||
@@ -275,13 +281,20 @@ static void do_trigger_list(uint8_t index, uint8_t button_type)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
||||||
uint8_t button_type, uint8_t modifier)
|
uint8_t button_type, uint8_t modifier) __banked
|
||||||
{
|
{
|
||||||
uint8_t link_timer = get_doorlink_timer(modifier);
|
uint8_t link_timer = get_doorlink_timer(modifier);
|
||||||
if (link_timer != 0x1F) { /* не заклинено */
|
if (link_timer != 0x1F) { /* не заклинено */
|
||||||
set_doorlink_timer(modifier, 5);
|
/* Оригинал ставит 5; вынесено в pop_tune.h, потому что проверять
|
||||||
if (link_timer < 2)
|
* руками проход в закрывающуюся решётку при таком окне почти
|
||||||
pop_add_trob(room, tilepos, 1); /* кнопка нажимается (+ TODO sound) */
|
* невозможно (см. BUG-GATE-PASS-1). */
|
||||||
|
set_doorlink_timer(modifier, POP_DBG_GATE_HOLD);
|
||||||
|
if (link_timer < 2) {
|
||||||
|
pop_add_trob(room, tilepos, 1); /* кнопка нажимается */
|
||||||
|
/* seg007:766: щелчок кнопки — шум, страж оборачивается. Звука у
|
||||||
|
* нас нет, флаг обязан быть (BUG-GUARD-DEAF-1). */
|
||||||
|
is_guard_notice = 1;
|
||||||
|
}
|
||||||
do_trigger_list(modifier, button_type);
|
do_trigger_list(modifier, button_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,7 +366,7 @@ static void animate_door(uint8_t *m, int8_t *type)
|
|||||||
*m = mod;
|
*m = mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t pop_trob_room_active(uint8_t room)
|
uint8_t pop_trob_room_active(uint8_t room) __banked
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
for (i = 0; i < trobs_count; i++)
|
for (i = 0; i < trobs_count; i++)
|
||||||
@@ -361,38 +374,35 @@ uint8_t pop_trob_room_active(uint8_t room)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t torch_tick; /* делитель скорости анимации пламени */
|
|
||||||
|
|
||||||
void pop_process_trobs(uint8_t cur_room)
|
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 3
|
||||||
|
* vsync и соответствует игровому тику оригинала):
|
||||||
|
* 1 — кадр пламени каждый тик, как в оригинале; кадры при этом ровные по
|
||||||
|
* тактам, что нужно для счёта бюджета;
|
||||||
|
* 2 — вдвое реже (так было до 2026-07-30; пламя шло на половинной
|
||||||
|
* скорости, а бюджет кадра «прыгал» на 6.2 % от того, попал кадр на
|
||||||
|
* анимацию или нет). */
|
||||||
|
#define TORCH_ANIM_DIV 1
|
||||||
|
|
||||||
|
#if TORCH_ANIM_DIV > 1
|
||||||
|
static uint8_t torch_tick;
|
||||||
|
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
|
||||||
|
#else
|
||||||
|
#define TORCH_ANIM_NOW 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void pop_process_trobs(uint8_t cur_room) __banked
|
||||||
{
|
{
|
||||||
uint8_t i, w = 0;
|
uint8_t i, w = 0;
|
||||||
|
#if TORCH_ANIM_DIV > 1
|
||||||
torch_tick++;
|
torch_tick++;
|
||||||
|
#endif
|
||||||
|
|
||||||
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
|
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
|
||||||
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
|
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
|
||||||
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
|
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
|
||||||
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
|
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
|
||||||
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
|
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
|
||||||
if (rest_pending) {
|
|
||||||
uint8_t row = 0, col = 0, left = 0;
|
|
||||||
for (i = 0; i < POP_ROOMTILES; i++) {
|
|
||||||
if (spike_rest[i]) {
|
|
||||||
pop_spike_redraw(row, col);
|
|
||||||
if (--spike_rest[i]) left++;
|
|
||||||
}
|
|
||||||
if (button_rest[i]) {
|
|
||||||
pop_button_redraw(row, col);
|
|
||||||
if (--button_rest[i]) left++;
|
|
||||||
}
|
|
||||||
if (++col == 10) { col = 0; row++; }
|
|
||||||
}
|
|
||||||
rest_pending = left;
|
|
||||||
}
|
|
||||||
if (ldoor_rest) { /* доложить финальный кадр створки на 2-ю страницу */
|
|
||||||
pop_leveldoor_redraw(ldoor_pos / 10, ldoor_pos % 10);
|
|
||||||
ldoor_rest--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trobs_count == 0) return;
|
if (trobs_count == 0) return;
|
||||||
|
|
||||||
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
|
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
|
||||||
@@ -430,11 +440,9 @@ void pop_process_trobs(uint8_t cur_room)
|
|||||||
break;
|
break;
|
||||||
case TILE_TORCH:
|
case TILE_TORCH:
|
||||||
case TILE_TORCH_DEBRIS:
|
case TILE_TORCH_DEBRIS:
|
||||||
/* animate_torch (seg007:03C1): кадр пламени; анимация вечная —
|
/* animate_torch (seg007:03C1): анимация вечная — type остаётся
|
||||||
* type остаётся 1, пока комната отрисована. Смена кадра — через
|
* 1, пока комната отрисована. Темп — TORCH_ANIM_DIV. */
|
||||||
* ЛОГИЧЕСКИЙ кадр (torch_tick): у нас кадр короче игрового тика
|
if (room == cur_room && TORCH_ANIM_NOW)
|
||||||
* оригинала, и пламя «кипело» вдвое быстрее нужного. */
|
|
||||||
if (room == cur_room && (torch_tick & 1) == 0)
|
|
||||||
mod[tp] = get_torch_frame(mod[tp]);
|
mod[tp] = get_torch_frame(mod[tp]);
|
||||||
break;
|
break;
|
||||||
case TILE_POTION:
|
case TILE_POTION:
|
||||||
@@ -477,24 +485,29 @@ void pop_process_trobs(uint8_t cur_room)
|
|||||||
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с
|
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с
|
||||||
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
|
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
|
||||||
* странице дабл-буфера из двух). */
|
* странице дабл-буфера из двух). */
|
||||||
uint8_t trow = tp / 10, tcol = tp % 10;
|
/* Последний кадр (пика убралась, trob умирает) — пометить ОБЕ
|
||||||
pop_spike_redraw(trow, tcol);
|
* страницы: живой trob метит каждый кадр по одной. */
|
||||||
/* Последний кадр (пика убралась, trob умирает) — на ВТОРУЮ страницу
|
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
|
||||||
* его уже никто не положит: ставим отложенный редрой. */
|
}
|
||||||
if (type < 0) { spike_rest[tp] = 1; rest_pending++; }
|
if (room == cur_room && code == TILE_GATE) {
|
||||||
|
/* draw_trob (seg007:01E6), которым заканчивается animate_door:
|
||||||
|
* пока trob жив — решётка едет каждый кадр, помечаем ТЕКУЩУЮ
|
||||||
|
* страницу; последний кадр (type<0, ворота встали) — ОБЕ, иначе
|
||||||
|
* на второй странице дабл-буфера застынет предыдущее положение
|
||||||
|
* и решётка дрожит через кадр. Тайлпос — САМИ ворота, соседа
|
||||||
|
* справа выбирает pop_gate_redraw. */
|
||||||
|
pop_set_redraw(tp, POP_RD_GATE, (uint8_t)(type < 0 ? 2 : 1));
|
||||||
}
|
}
|
||||||
if (room == cur_room && code == TILE_LEVELDOOR) {
|
if (room == cur_room && code == TILE_LEVELDOOR) {
|
||||||
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
|
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
|
||||||
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
|
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
|
||||||
* kid_heal вернул бы из фона закрытую створку. Куски двери
|
* kid_heal вернул бы из фона закрытую створку. Куски двери
|
||||||
* непрозрачны, поэтому wipe не нужен. */
|
* непрозрачны, поэтому wipe не нужен. */
|
||||||
uint8_t trow = tp / 10, tcol = tp % 10;
|
/* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
|
||||||
if (tcol + 1 < 10) {
|
* помечаем на обе страницы, иначе дверь дрожит через кадр. */
|
||||||
pop_leveldoor_redraw(trow, tcol + 1);
|
if (tp % 10 + 1 < 10)
|
||||||
/* Анимация кончилась — этот кадр лёг только на ОДНУ страницу;
|
pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR,
|
||||||
* повторить его на второй, иначе дверь дрожит через кадр. */
|
(uint8_t)(type < 0 ? 2 : 1));
|
||||||
if (type < 0) { ldoor_pos = (uint8_t)(tp + 1); ldoor_rest = 1; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (room == cur_room && code == TILE_SWORD) {
|
if (room == cur_room && code == TILE_SWORD) {
|
||||||
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
|
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
|
||||||
@@ -503,9 +516,7 @@ void pop_process_trobs(uint8_t cur_room)
|
|||||||
* следующим кадром через rest-цикл. */
|
* следующим кадром через rest-цикл. */
|
||||||
uint8_t sig = (uint8_t)(mod[tp] == 1);
|
uint8_t sig = (uint8_t)(mod[tp] == 1);
|
||||||
if (sig != trob_drawn[tp]) {
|
if (sig != trob_drawn[tp]) {
|
||||||
uint8_t trow = tp / 10, tcol = tp % 10;
|
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
|
||||||
pop_button_redraw(trow, tcol);
|
|
||||||
button_rest[tp] = 1; rest_pending++;
|
|
||||||
trob_drawn[tp] = sig;
|
trob_drawn[tp] = sig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,9 +528,7 @@ void pop_process_trobs(uint8_t cur_room)
|
|||||||
* считаем только в момент редроя (редко), а не каждый кадр. */
|
* считаем только в момент редроя (редко), а не каждый кадр. */
|
||||||
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
|
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
|
||||||
if (sig != trob_drawn[tp]) {
|
if (sig != trob_drawn[tp]) {
|
||||||
uint8_t trow = tp / 10, tcol = tp % 10;
|
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
|
||||||
pop_button_redraw(trow, tcol);
|
|
||||||
button_rest[tp] = 1; rest_pending++;
|
|
||||||
trob_drawn[tp] = sig;
|
trob_drawn[tp] = sig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,41 +25,41 @@
|
|||||||
|
|
||||||
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
|
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
|
||||||
* очистить список trob. */
|
* очистить список trob. */
|
||||||
void pop_trob_reset(void);
|
void pop_trob_reset(void) __banked;
|
||||||
|
|
||||||
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
|
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
|
||||||
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
|
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
|
||||||
* (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне
|
* (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне
|
||||||
* диапазона → 0. */
|
* диапазона → 0. */
|
||||||
uint8_t *pop_trob_modif(uint8_t room);
|
uint8_t *pop_trob_modif(uint8_t room) __banked;
|
||||||
|
|
||||||
/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить
|
/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить
|
||||||
* type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */
|
* type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */
|
||||||
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type);
|
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked;
|
||||||
|
|
||||||
/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать
|
/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать
|
||||||
* modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице.
|
* modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице.
|
||||||
* Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */
|
* Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */
|
||||||
void pop_process_trobs(uint8_t cur_room);
|
void pop_process_trobs(uint8_t cur_room) __banked;
|
||||||
|
|
||||||
/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку
|
/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку
|
||||||
* LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button.
|
* LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button.
|
||||||
* room/tilepos — кнопка; button_type — её тайл-код; modifier — её modif
|
* room/tilepos — кнопка; button_type — её тайл-код; modifier — её modif
|
||||||
* (= индекс в LINKLOC). Звать из check_press (pop_map). */
|
* (= индекс в LINKLOC). Звать из check_press (pop_map). */
|
||||||
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
||||||
uint8_t button_type, uint8_t modifier);
|
uint8_t button_type, uint8_t modifier) __banked;
|
||||||
|
|
||||||
/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через
|
/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через
|
||||||
* шов (roomtest не дёргает шов, пока сосед не анимируется). */
|
* шов (roomtest не дёргает шов, пока сосед не анимируется). */
|
||||||
uint8_t pop_trob_room_active(uint8_t room);
|
uint8_t pop_trob_room_active(uint8_t room) __banked;
|
||||||
|
|
||||||
/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты
|
/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты
|
||||||
* (trob-состояние в room_modif персистентно и НЕ трогается). Звать в
|
* (trob-состояние в room_modif персистентно и НЕ трогается). Звать в
|
||||||
* enter_room. */
|
* enter_room. */
|
||||||
void pop_trob_room_changed(void);
|
void pop_trob_room_changed(void) __banked;
|
||||||
|
|
||||||
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
|
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
|
||||||
* зелья) со случайной стартовой фазой. Звать в enter_room. */
|
* зелья) со случайной стартовой фазой. Звать в enter_room. */
|
||||||
void pop_trob_anim_room(uint8_t room);
|
void pop_trob_anim_room(uint8_t room) __banked;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* pop_tune.h — НАСТРАИВАЕМЫЕ КОНСТАНТЫ движка (аналог custom_options
|
||||||
|
* SDLPoP) в одном месте.
|
||||||
|
*
|
||||||
|
* Зачем отдельный заголовок. У оригинала «особые события» уровней зашиты
|
||||||
|
* магическими числами прямо в код (`current_level == 3 && Char.room == 7`),
|
||||||
|
* и SDLPoP вынес их в структуру `custom_options_type` (types.h) с
|
||||||
|
* загрузкой из SDLPoP.ini и правкой через меню Settings/Mods. Нам нужно
|
||||||
|
* то же самое по двум причинам:
|
||||||
|
*
|
||||||
|
* 1. ОТЛАДКА. Некоторые сцены руками почти не проверить (успеть в
|
||||||
|
* закрывающуюся решётку), и единственный вменяемый способ — временно
|
||||||
|
* растянуть тайминг. Такие крутилки обязаны жить в одном месте, а не
|
||||||
|
* прятаться числами по трём модулям.
|
||||||
|
* 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа.
|
||||||
|
*
|
||||||
|
* Пока это только `#define` — ни INI, ни меню нет (см. TASKS.md, запись
|
||||||
|
* TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг
|
||||||
|
* заводится ЗДЕСЬ, а не константой по месту. Имена — как у SDLPoP
|
||||||
|
* (custom->имя), чтобы сверка с оригиналом оставалась механической.
|
||||||
|
*/
|
||||||
|
#ifndef POP_TUNE_H
|
||||||
|
#define POP_TUNE_H
|
||||||
|
|
||||||
|
/* ---- Чекпойнт уровня 3 (SDLPoP: checkpoint_*) ----------------------- *
|
||||||
|
* Оригинал: level3_set_chkp (seg002:0665) взводит флаг, когда Кид уходит
|
||||||
|
* ВЛЕВО ИЗ комнаты 7 (вызов стоит до goto_other_room, поэтому Char.room там
|
||||||
|
* ещё старая); do_startpos (seg003:0141) подменяет по
|
||||||
|
* нему стартовую позицию и убирает loose-плиту, через которую туда
|
||||||
|
* попадают (иначе после респавна дорога назад была бы уже провалена). */
|
||||||
|
#define POP_CHKP_LEVEL 3 /* checkpoint_level */
|
||||||
|
#define POP_CHKP_ROOM 7 /* комната, ИЗ КОТОРОЙ уходят влево (у
|
||||||
|
* SDLPoP хардкод; это НЕ комната
|
||||||
|
* назначения — см. roomtest.c) */
|
||||||
|
#define POP_CHKP_RESPAWN_ROOM 2 /* checkpoint_respawn_room */
|
||||||
|
#define POP_CHKP_RESPAWN_TILEPOS 6 /* checkpoint_respawn_tilepos */
|
||||||
|
#define POP_CHKP_RESPAWN_DIR (-1) /* checkpoint_respawn_dir: влево */
|
||||||
|
#define POP_CHKP_CLEAR_ROOM 7 /* checkpoint_clear_tile_room */
|
||||||
|
#define POP_CHKP_CLEAR_COL 4 /* checkpoint_clear_tile_col */
|
||||||
|
#define POP_CHKP_CLEAR_ROW 0 /* checkpoint_clear_tile_row */
|
||||||
|
|
||||||
|
/* ---- Отладочные крутилки (в оригинале и в SDLPoP их НЕТ) ------------ *
|
||||||
|
* POP_DBG_GATE_HOLD — сколько кадров решётка держится поднятой после
|
||||||
|
* нажатия кнопки, ПРЕЖДЕ чем пойдёт вниз. В оригинале это таймер связи
|
||||||
|
* дверей: trigger_button ставит 5, animate_button вычитает по единице, и
|
||||||
|
* решётка стоит, пока таймер > 1 (то есть ~4 кадра после того, как Кид
|
||||||
|
* сошёл с кнопки). Проверять руками проход в закрывающуюся решётку при
|
||||||
|
* таком окне почти невозможно, поэтому значение вынесено сюда: поставь
|
||||||
|
* 30 — и успеешь дойти спокойно. ОБЯЗАН быть 5 в игровой сборке.
|
||||||
|
*
|
||||||
|
* ПОТОЛОК 30. Таймер связи — пять бит модификатора кнопки (маска 0x1F), а
|
||||||
|
* значение 31 у оригинала означает «событие заклинено» (`link_timer != 0x1F`
|
||||||
|
* в trigger_button). Больше 30 ставить нельзя — кнопка перестанет
|
||||||
|
* срабатывать вовсе. */
|
||||||
|
#define POP_DBG_GATE_HOLD 5
|
||||||
|
|
||||||
|
/* ---- Улучшения SDLPoP (в ОРИГИНАЛЕ их нет) -------------------------- *
|
||||||
|
* POP_ENABLE_JUMP_GRAB — зацепиться за кромку можно ПРЯМО В ПРЫЖКЕ, если
|
||||||
|
* Shift зажат заранее (SDLPoP `enable_jump_grab`, seg006:1228). Ваниль
|
||||||
|
* разрешает зацеп только в начале падения (`check_grab`, кадры 102..105),
|
||||||
|
* то есть Shift приходится нажимать уже в полёте — это и раздражало на
|
||||||
|
* приёмке. 0 — как в оригинале, 1 — как в SDLPoP с включёнными
|
||||||
|
* enhancements. */
|
||||||
|
#define POP_ENABLE_JUMP_GRAB 1
|
||||||
|
|
||||||
|
#endif /* POP_TUNE_H */
|
||||||
@@ -15,20 +15,91 @@
|
|||||||
#include "pop_map.h"
|
#include "pop_map.h"
|
||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "pop_trob.h"
|
#include "pop_trob.h"
|
||||||
|
#include "pop_redraw.h"
|
||||||
|
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
|
||||||
|
#include "pop_guard.h"
|
||||||
|
#include "pop_cheat.h"
|
||||||
|
#include "pop_tune.h"
|
||||||
|
|
||||||
__sfr __at (0xFE) io_border;
|
__sfr __at (0xFE) io_border;
|
||||||
|
|
||||||
|
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
|
||||||
|
* выключить). Цвет бордюра держится, пока фаза считает, поэтому высота
|
||||||
|
* полосы на скриншоте = время фазы в растровых строках. Это единственный
|
||||||
|
* способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
|
||||||
|
* отвечает из фреймового колбэка, все сэмплы падают в обработчик
|
||||||
|
* прерывания); точные такты — breakpoint'ы с {printf totalcycles; g}.
|
||||||
|
*
|
||||||
|
* ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
|
||||||
|
* бит 1 (синий) и бит 2 (зелёный) — значит различимых состояний ровно
|
||||||
|
* ЧЕТЫРЕ, и значения обязаны быть чётными. Отсюда фазы сгруппированы по
|
||||||
|
* четыре, а не по семь (нечётные номера раньше сливались с соседями и
|
||||||
|
* полосы не читались):
|
||||||
|
* 0 чёрный — ожидание vsync (запас кадра)
|
||||||
|
* 2 синий — ввод, heal и ЛОГИКА (kid_tick/физика/страж)
|
||||||
|
* 4 зелёный — слой ФОНА (loose/trobs/redraw_needed/шов)
|
||||||
|
* 6 циан — СПРАЙТЫ (страж, Kid, fore поверх них) */
|
||||||
|
#ifndef PROF_BORDER
|
||||||
|
#define PROF_BORDER 1
|
||||||
|
#endif
|
||||||
|
#if PROF_BORDER
|
||||||
|
#define PROF(n) io_border = (n)
|
||||||
|
#else
|
||||||
|
#define PROF(n) ((void)0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Сколько банков грузит crt0_banked (читает его стартовый код).
|
||||||
|
* 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
|
||||||
|
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP),
|
||||||
|
* 5 = pop_ctrl (диспетчер управления).
|
||||||
|
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
|
||||||
|
* вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних
|
||||||
|
* уровней, чомперы, зеркало.
|
||||||
|
*
|
||||||
|
* ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть — это не ошибка
|
||||||
|
* сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и
|
||||||
|
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
|
||||||
|
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
|
||||||
|
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */
|
||||||
|
const uint8_t n_banks = 6;
|
||||||
|
|
||||||
|
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
||||||
|
uint8_t pop_cheats;
|
||||||
|
|
||||||
|
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
|
||||||
|
*
|
||||||
|
* При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и
|
||||||
|
* одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание — тоже
|
||||||
|
* 5). Приёмный FIFO SIO — 3 байта, а импульс запроса прерывания здесь
|
||||||
|
* теряется примерно в 44 % случаев, поэтому байты копятся и один из
|
||||||
|
* полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
|
||||||
|
*
|
||||||
|
* Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько
|
||||||
|
* вызовов за кадр — они попадают туда, где прерывания и так разрешены, и
|
||||||
|
* лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
|
||||||
|
* без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя — машина
|
||||||
|
* уходит в перезагрузку.
|
||||||
|
*
|
||||||
|
* Что помогает: опрос раз в ~0.5 мс. Столько времени берётся из ожидания
|
||||||
|
* кадра — процессор всё равно крутит там ~42 мс из 60, ничего не делая.
|
||||||
|
* Поэтому kbd_raw_poll вешается idle-хуком на gfx_wait_vsync (см.
|
||||||
|
* kbd_idle ниже); полезной работы это не отнимает. Потолок приёма
|
||||||
|
* подтверждён отдельно (tests/kbdpoll: 25 нажатий → 25, 250 байт из 250). */
|
||||||
|
|
||||||
|
/* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает
|
||||||
|
* «было ли что вычерпано». Приводить указатель кастом не хочется. */
|
||||||
|
static void kbd_idle(void)
|
||||||
|
{
|
||||||
|
kbd_raw_poll();
|
||||||
|
}
|
||||||
|
|
||||||
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
|
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
|
||||||
#define SEQ_STAND 2
|
#define SEQ_STAND 2
|
||||||
#define ROOM_1 1
|
#define ROOM_1 1
|
||||||
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
||||||
|
|
||||||
/* Тест-старт для отладки пик (Фаза S): комната 6 = тестовая (пики в col3/col4
|
#define FIRST_LEVEL 1 /* с какого уровня начинается игра */
|
||||||
* row2, шахта col3). Kid на полу row0 col1, идёт вправо → падает в шахту →
|
|
||||||
* пики. TODO: вернуть на pop_level_start_room (комната 1) после отладки. */
|
|
||||||
#define START_ROOM 1 /* обход комнат: старт с 1-й, дальше +/- (ROOMNAV) */
|
|
||||||
#define START_COL 3
|
|
||||||
#define START_ROW 0
|
|
||||||
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
|
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
|
||||||
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
|
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
|
||||||
|
|
||||||
@@ -39,6 +110,7 @@ __sfr __at (0xFE) io_border;
|
|||||||
* Убирается закомментированием ОДНОЙ строки ниже. */
|
* Убирается закомментированием ОДНОЙ строки ниже. */
|
||||||
#define ROOMNAV 1
|
#define ROOMNAV 1
|
||||||
|
|
||||||
|
|
||||||
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
|
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
|
||||||
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
|
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
|
||||||
* чтобы enter_room переключал комнату без длинного списка параметров. */
|
* чтобы enter_room переключал комнату без длинного списка параметров. */
|
||||||
@@ -46,7 +118,7 @@ static uint8_t room_fg[30], room_bg[30];
|
|||||||
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
|
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
|
||||||
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
|
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
|
||||||
* смотрит наружу (колонка −2 / 11). См. pop_level.h. */
|
* смотрит наружу (колонка −2 / 11). См. pop_level.h. */
|
||||||
static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[10];
|
static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11];
|
||||||
static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
|
static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
|
||||||
static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
|
static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
|
||||||
static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
|
static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
|
||||||
@@ -64,6 +136,89 @@ static void update_kid_render_dx(void)
|
|||||||
pop_kid_set_render_dx(dx);
|
pop_kid_set_render_dx(dx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Порядок «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 = страж рисуется ПОСЛЕ Кида (поверх). */
|
||||||
|
static uint8_t guard_over_kid(void)
|
||||||
|
{
|
||||||
|
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(), kid_fp_width());
|
||||||
|
objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy,
|
||||||
|
pop_gframe.flags, pop_guard_fp_width());
|
||||||
|
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 > kid_fp_obj_y());
|
||||||
|
}
|
||||||
|
|
||||||
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
|
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
|
||||||
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
|
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
|
||||||
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
|
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
|
||||||
@@ -77,17 +232,15 @@ static uint8_t seam_redraw; /* оставшиеся кадры редроя (
|
|||||||
static uint8_t room_label_sig = 0xFF;
|
static uint8_t room_label_sig = 0xFF;
|
||||||
static uint8_t room_label_redraw;
|
static uint8_t room_label_redraw;
|
||||||
|
|
||||||
/* Персистентные переопределения тайлов уровня: упавший loose-кусок оставил
|
/* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления
|
||||||
* щебень (floor → debris), а на месте самой плиты — пусто (в т.ч. в комнате
|
* куска, поднятый предмет → пол) пишутся ПРЯМО В УРОВЕНЬ —
|
||||||
* СВЕРХУ: провалившийся потолок = колодец). Минимальный per-room override:
|
* pop_level_set_tile, как в оригинале (curr_room_tiles[…] = …). Раньше тут
|
||||||
* живёт между переходами, применяется в enter_room поверх загруженных тайлов
|
* жила таблица переопределений на 8 записей, и девятая правка молча
|
||||||
* (и для ряда 2 верхнего соседа). (Полноценное per-room modifier-состояние —
|
* терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
|
||||||
* в docs/gates_spikes_plan.md, P0.) */
|
* комнату (BUG-LVLSTATE-1). */
|
||||||
#define TILE_DEBRIS 0x0E
|
#define TILE_DEBRIS 0x0E
|
||||||
#define TILE_EMPTY 0x00
|
#define TILE_EMPTY 0x00
|
||||||
#define TILE_FLOOR 0x01
|
#define TILE_FLOOR 0x01
|
||||||
#define OVR_MAX 8
|
|
||||||
static uint8_t ovr_room[OVR_MAX], ovr_pos[OVR_MAX], ovr_tile[OVR_MAX], ovr_cnt;
|
|
||||||
|
|
||||||
/* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
|
/* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
|
||||||
* flash_on — вспышка фона активна (нулевая запись палитры подменена). */
|
* flash_on — вспышка фона активна (нулевая запись палитры подменена). */
|
||||||
@@ -103,36 +256,15 @@ static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
|
|||||||
gfx_pal_set(1, 0, r, g, b);
|
gfx_pal_set(1, 0, r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void add_tile_override(uint8_t room, uint8_t pos, uint8_t tile)
|
|
||||||
{
|
|
||||||
uint8_t i;
|
|
||||||
for (i = 0; i < ovr_cnt; i++)
|
|
||||||
if (ovr_room[i] == room && ovr_pos[i] == pos) { ovr_tile[i] = tile; return; }
|
|
||||||
if (ovr_cnt < OVR_MAX) {
|
|
||||||
ovr_room[ovr_cnt] = room; ovr_pos[ovr_cnt] = pos;
|
|
||||||
ovr_tile[ovr_cnt] = tile; ovr_cnt++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Применить переопределения комнаты room к массиву тайлов: base = tilepos
|
|
||||||
* первого элемента (0 для полной комнаты, 20 для ряда 2 верхнего соседа). */
|
|
||||||
static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t n)
|
|
||||||
{
|
|
||||||
uint8_t i;
|
|
||||||
for (i = 0; i < ovr_cnt; i++)
|
|
||||||
if (ovr_room[i] == room && ovr_pos[i] >= base && ovr_pos[i] < base + n)
|
|
||||||
tiles[ovr_pos[i] - base] = ovr_tile[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
|
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
|
||||||
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
|
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
|
||||||
* активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
|
* активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
|
||||||
static void enter_room(uint8_t room)
|
static void enter_room(uint8_t room)
|
||||||
{
|
{
|
||||||
uint8_t pg;
|
uint8_t pg;
|
||||||
|
pop_guard_leave(); /* запомнить стража уходящей комнаты */
|
||||||
uint8_t *room_mod;
|
uint8_t *room_mod;
|
||||||
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
|
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
|
||||||
apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */
|
|
||||||
pop_map_set(room_fg);
|
pop_map_set(room_fg);
|
||||||
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
|
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
|
||||||
pop_room_link(room, 2), pop_room_link(room, 3),
|
pop_room_link(room, 2), pop_room_link(room, 3),
|
||||||
@@ -147,7 +279,6 @@ static void enter_room(uint8_t room)
|
|||||||
pop_level_access_begin();
|
pop_level_access_begin();
|
||||||
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
|
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
|
||||||
pop_level_access_end();
|
pop_level_access_end();
|
||||||
apply_overrides(up, above_fg, 20, 10); /* провалившийся потолок = колодец */
|
|
||||||
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
|
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
|
||||||
pop_room_set_above(above_fg, above_mod);
|
pop_room_set_above(above_fg, above_mod);
|
||||||
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
|
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
|
||||||
@@ -157,9 +288,16 @@ static void enter_room(uint8_t room)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pop_map_set_room(room);
|
pop_map_set_room(room);
|
||||||
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
|
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
|
||||||
|
* (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
|
||||||
|
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
|
||||||
|
* стража никогда не срабатывает. */
|
||||||
|
Kid.room = room;
|
||||||
|
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
|
||||||
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
|
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
|
||||||
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
|
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
|
||||||
|
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
|
||||||
|
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
|
||||||
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
||||||
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
||||||
* а не по свежему backtable. Фаза P0. */
|
* а не по свежему backtable. Фаза P0. */
|
||||||
@@ -187,6 +325,119 @@ static void enter_room(uint8_t room)
|
|||||||
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
|
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
|
|
||||||
|
/* 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) */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7
|
||||||
|
* уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет
|
||||||
|
* стартовую позицию. Сбрасывается началом игры и прохождением уровня —
|
||||||
|
* ровно как в оригинале (seg003:37 и seg003:413). */
|
||||||
|
static uint8_t pop_checkpoint;
|
||||||
|
|
||||||
|
static void pop_start_level(void)
|
||||||
|
{
|
||||||
|
uint8_t entry_pose = pop_level_entry_pose(pop_current_level);
|
||||||
|
uint8_t start_room = pop_level_start_room();
|
||||||
|
uint8_t pos = pop_level_start_pos();
|
||||||
|
int8_t dir = (int8_t)~pop_level_start_dir();
|
||||||
|
uint8_t seq = SEQ_TURN;
|
||||||
|
/* Особое событие: старт с чекпойнта (do_startpos, seg003:0141).
|
||||||
|
* Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles()
|
||||||
|
* ниже, иначе рестарт её вернёт. */
|
||||||
|
uint8_t 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);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
|
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
|
||||||
@@ -197,25 +448,36 @@ int main(void)
|
|||||||
#ifdef ROOMNAV
|
#ifdef ROOMNAV
|
||||||
uint8_t nav_prev = 0;
|
uint8_t nav_prev = 0;
|
||||||
#endif
|
#endif
|
||||||
|
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;
|
||||||
|
uint8_t xdec_prev = 0, xinc_prev = 0;
|
||||||
|
|
||||||
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
|
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
|
||||||
puts("pop_bg_load failed");
|
puts("pop_bg_load failed");
|
||||||
return 1;
|
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_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
|
||||||
|
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
||||||
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
||||||
puts("kid atlas load failed");
|
puts("kid atlas load failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (pop_level_load("res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */
|
if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */
|
||||||
pop_level_load("a:\\res2001.bin") != 0) {
|
|
||||||
puts("level load failed");
|
puts("level load failed");
|
||||||
return 1;
|
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();
|
initgraph();
|
||||||
gfx_set_visible_page(0);
|
gfx_set_visible_page(0);
|
||||||
if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */
|
if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */
|
||||||
gfx_pal_fload(0, "a:\\kid.pal");
|
gfx_pal_fload(0, "a:\\kid.pal");
|
||||||
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
|
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
|
||||||
|
|
||||||
@@ -225,15 +487,11 @@ int main(void)
|
|||||||
* ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
|
* ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
|
||||||
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
|
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
|
||||||
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
|
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
|
||||||
pop_trob_reset();
|
/* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
|
||||||
enter_room(START_ROOM);
|
* pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места,
|
||||||
|
* Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */
|
||||||
|
pop_start_level();
|
||||||
gfx_set_draw_page(0);
|
gfx_set_draw_page(0);
|
||||||
|
|
||||||
/* K2b: Kid стоит на уступе (row1), управляется с клавиатуры.
|
|
||||||
* ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход.
|
|
||||||
* Карта коллизии (pop_map_set) уже задана в enter_room. */
|
|
||||||
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
|
|
||||||
pop_kid_hp_reset();
|
|
||||||
kid_draw();
|
kid_draw();
|
||||||
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
||||||
kid_fp_width(), kid_fp_height(),
|
kid_fp_width(), kid_fp_height(),
|
||||||
@@ -244,12 +502,13 @@ int main(void)
|
|||||||
puts("kbd_raw open failed");
|
puts("kbd_raw open failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
|
||||||
|
|
||||||
while (!kbd_raw_down(KBD_ESC)) {
|
while (!kbd_raw_down(KBD_ESC)) {
|
||||||
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||||
* переключ). Выключение возвращает показ на страницу 0. */
|
* переключ). Выключение возвращает показ на страницу 0. */
|
||||||
|
|
||||||
io_border = 2; /* синий - начало */
|
PROF(2); /* ввод + heal */
|
||||||
|
|
||||||
uint8_t sp = kbd_raw_down(KBD_SPACE);
|
uint8_t sp = kbd_raw_down(KBD_SPACE);
|
||||||
if (sp && !sp_prev) {
|
if (sp && !sp_prev) {
|
||||||
@@ -266,8 +525,50 @@ int main(void)
|
|||||||
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
|
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
|
||||||
if (kbd_raw_down(0x16)) frozen = 1;
|
if (kbd_raw_down(0x16)) frozen = 1;
|
||||||
if (kbd_raw_down(0x1E)) frozen = 0;
|
if (kbd_raw_down(0x1E)) frozen = 0;
|
||||||
|
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
|
||||||
|
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
|
||||||
|
* K — убить стража;
|
||||||
|
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
|
||||||
|
* иначе боёвку проверять получается ровно три раза);
|
||||||
|
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
|
||||||
|
* берётся с пола (pop_have_sword ставит подбор);
|
||||||
|
* Shift+L — следующий уровень (seg000:698, ветка next_level =
|
||||||
|
* current_level + 1). Единственный вменяемый способ
|
||||||
|
* тестировать уровни 2..15. NB: L без Shift — отладочный
|
||||||
|
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
|
||||||
|
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
|
||||||
|
* что конфликта на практике нет;
|
||||||
|
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
|
||||||
|
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
|
||||||
|
* теряет нажатия при быстрой отправке, и подогнать Кида в
|
||||||
|
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
|
||||||
|
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
|
||||||
|
* персонажа в этом порте задаёт не диспетчер (грабли
|
||||||
|
* L1-EXIT). Колонку не трогаем — её пересчитает физика
|
||||||
|
* следующего кадра. */
|
||||||
|
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
|
||||||
|
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
|
||||||
|
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
|
||||||
|
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
|
||||||
|
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
|
||||||
|
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
|
||||||
|
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
|
||||||
|
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
|
||||||
|
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
|
||||||
|
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
|
||||||
|
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
|
||||||
|
pop_next_level = (uint8_t)(pop_current_level + 1);
|
||||||
|
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
|
||||||
|
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
|
||||||
|
cheat_prev = k;
|
||||||
|
immo_prev = i;
|
||||||
|
sword_prev = s;
|
||||||
|
nlvl_prev = nl;
|
||||||
|
xdec_prev = xd;
|
||||||
|
xinc_prev = xi;
|
||||||
|
}
|
||||||
if (frozen) {
|
if (frozen) {
|
||||||
io_border = 0; /* черный: ждём vsync */
|
PROF(0); /* чёрный: ждём vsync */
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -275,8 +576,17 @@ int main(void)
|
|||||||
#ifdef ROOMNAV
|
#ifdef ROOMNAV
|
||||||
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
|
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
|
||||||
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
|
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
|
||||||
* комната. pop_trob_reset ДО enter_room — каждая комната смотрится в
|
* комната.
|
||||||
* исходном состоянии (пики убраны, ворота закрыты). */
|
*
|
||||||
|
* Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната
|
||||||
|
* смотрелась в исходном состоянии». Убран: reset обнуляет room_seen,
|
||||||
|
* и модификаторы перечитываются из уровня заново, то есть чит
|
||||||
|
* ОТМАТЫВАЕТ МИР — открытые/закрытые ворота, выдвинутые пики, нажатые
|
||||||
|
* кнопки. Пока ворота с bg=1 ошибочно стартовали закрытыми, откат был
|
||||||
|
* не виден; после порта load_alter_mod он выглядит как баг «ворота
|
||||||
|
* снова открылись» (уровень 1, решётка комнаты 5 (0,9), которую
|
||||||
|
* закрывает стартовый триггер do_startpos). Навигация обязана только
|
||||||
|
* телепортировать; исходное состояние даёт перезапуск уровня. */
|
||||||
{
|
{
|
||||||
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
|
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
|
||||||
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
|
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
|
||||||
@@ -284,7 +594,6 @@ int main(void)
|
|||||||
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
|
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
|
||||||
r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
|
r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
|
||||||
: (uint8_t)(r == 1 ? 24 : r - 1);
|
: (uint8_t)(r == 1 ? 24 : r - 1);
|
||||||
pop_trob_reset();
|
|
||||||
enter_room(r);
|
enter_room(r);
|
||||||
/* Предпочитаем обычный пол; иначе — любой tile_is_floor
|
/* Предпочитаем обычный пол; иначе — любой tile_is_floor
|
||||||
* (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
|
* (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
|
||||||
@@ -307,41 +616,65 @@ int main(void)
|
|||||||
back = dbuf ? (uint8_t)(front ^ 1) : 0;
|
back = dbuf ? (uint8_t)(front ^ 1) : 0;
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
|
|
||||||
|
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
|
||||||
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||||
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
|
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
|
||||||
|
pop_guard_heal(); /* и прошлый кадр стража */
|
||||||
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
|
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
|
||||||
|
PROF(2); /* логика (тот же синий) */
|
||||||
kid_tick(); /* play_seq: следующий кадр */
|
kid_tick(); /* play_seq: следующий кадр */
|
||||||
pop_phys_tick(); /* K3: падение/приземление/стена */
|
pop_phys_tick(); /* K3: падение/приземление/стена */
|
||||||
|
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
|
||||||
|
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
|
||||||
|
* попадания с обеих сторон, потом применить, потом свести HP. */
|
||||||
|
pop_check_sword_hurting();
|
||||||
|
pop_check_sword_hurt();
|
||||||
|
pop_do_delta_hp();
|
||||||
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
|
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
|
||||||
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
||||||
pop_fell_out = 0;
|
pop_fell_out = 0;
|
||||||
if (down) { /* L2: переход в комнату снизу — продолжает падать */
|
if (down) { /* L2: переход в комнату снизу — продолжает падать */
|
||||||
enter_room(down); /* координаты Kid уже репроецированы pop_map */
|
enter_room(down); /* координаты Kid уже репроецированы pop_map */
|
||||||
gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */
|
gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */
|
||||||
} else { /* выпал из уровня (нет комнаты снизу) — респавн */
|
} else { /* выпал из уровня (нет комнаты снизу) — рестарт */
|
||||||
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */
|
pop_start_level();
|
||||||
enter_room(START_ROOM);
|
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
|
|
||||||
pop_kid_hp_reset();
|
|
||||||
}
|
}
|
||||||
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
|
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
|
||||||
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
|
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
|
||||||
|
uint8_t left = (uint8_t)(pop_leave_dir == 1);
|
||||||
pop_leave_dir = 0;
|
pop_leave_dir = 0;
|
||||||
|
/* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал
|
||||||
|
* зовёт его из leave_room ДО goto_other_room, то есть Char.room
|
||||||
|
* там ещё СТАРАЯ. Значит условие `Char.room == 7` читается как
|
||||||
|
* «Кид УХОДИТ ИЗ комнаты 7 влево» (в комнату 2), а не «входит в
|
||||||
|
* неё». Смысл: чекпойнт даётся за то, что комната 7 пройдена
|
||||||
|
* НАСКВОЗЬ, и респавн ставится туда, куда Кид только что попал.
|
||||||
|
*
|
||||||
|
* Сначала я поставил проверку на комнату НАЗНАЧЕНИЯ — и это
|
||||||
|
* поймал пользователь прогоном в SDLPoP: он умер В комнате 7, но
|
||||||
|
* влево из неё не выходил, и оригинал вернул его в стартовую
|
||||||
|
* комнату 9, а плита осталась цела. Заодно тут запоминается
|
||||||
|
* достигнутый потолок HP: смерть после чекпойнта его не отбирает. */
|
||||||
|
if (left && pop_current_level == POP_CHKP_LEVEL &&
|
||||||
|
cur_room == POP_CHKP_ROOM) {
|
||||||
|
pop_checkpoint = 1;
|
||||||
|
hitp_beg_lev = hitp_max;
|
||||||
|
}
|
||||||
if (nb) {
|
if (nb) {
|
||||||
enter_room(nb);
|
enter_room(nb);
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io_border = 4; /* зеленый */
|
PROF(4); /* слой фона */
|
||||||
|
|
||||||
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
|
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
|
||||||
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
|
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
|
||||||
io_border = 6; /* голубой */
|
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
|
||||||
pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */
|
/* process_trobs — рисования внутри нет) */
|
||||||
/* слой фона — ДО Kid) */
|
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
|
||||||
io_border = 4; /* синий */
|
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
|
||||||
{ /* ворота соседа слева (room8) видны в col0 — */
|
{ /* ворота соседа слева (room8) видны в col0 — */
|
||||||
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
|
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
|
||||||
if (lft) {
|
if (lft) {
|
||||||
@@ -358,46 +691,62 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
io_border = 2; /* зеленый */
|
if (pop_next_level != pop_current_level) {
|
||||||
|
/* Порт 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 {
|
||||||
|
/* Всё состояние прошлого уровня сносит сам pop_start_level:
|
||||||
|
* pop_trob_reset обнуляет room_seen (модификаторы комнат
|
||||||
|
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
|
||||||
|
* из неё же, loose чистит enter_room. */
|
||||||
|
pop_start_level();
|
||||||
|
}
|
||||||
|
gfx_set_draw_page(back);
|
||||||
|
}
|
||||||
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
|
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
|
||||||
dead_frames++;
|
dead_frames++;
|
||||||
if (dead_frames > RESPAWN_SETTLE &&
|
if (dead_frames > RESPAWN_SETTLE &&
|
||||||
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
|
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
|
||||||
dead_frames = 0;
|
dead_frames = 0;
|
||||||
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */
|
pop_start_level();
|
||||||
enter_room(START_ROOM);
|
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
|
|
||||||
pop_kid_hp_reset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
io_border = 6; /* голубой */
|
|
||||||
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
|
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
|
||||||
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
|
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
|
||||||
uint8_t fcol = (uint8_t)(fpos % 10);
|
uint8_t fcol = (uint8_t)(fpos % 10);
|
||||||
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
||||||
pop_loose_fell = 0;
|
pop_loose_fell = 0;
|
||||||
io_border = 2; /* синий */
|
pop_level_set_tile(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
|
||||||
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
|
|
||||||
if (down) {
|
if (down) {
|
||||||
int8_t lrow = pop_room_col_landing(down, fcol);
|
int8_t lrow = pop_room_col_landing(down, fcol);
|
||||||
if (lrow >= 0) add_tile_override(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
|
if (lrow >= 0) pop_level_set_tile(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
|
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
|
||||||
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
|
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
|
||||||
uint8_t up = pop_room_link(cur_room, 2);
|
uint8_t up = pop_room_link(cur_room, 2);
|
||||||
pop_ceil_fell = 0;
|
pop_ceil_fell = 0;
|
||||||
if (up) add_tile_override(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
|
if (up) pop_level_set_tile(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
|
||||||
}
|
}
|
||||||
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
|
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
|
||||||
add_tile_override(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
|
pop_level_set_tile(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
|
||||||
pop_debris_at = 0;
|
pop_debris_at = 0;
|
||||||
}
|
}
|
||||||
if (pop_item_taken) { /* предмет поднят → тайл стал полом */
|
if (pop_item_taken) { /* предмет поднят → тайл стал полом */
|
||||||
item_pos = (uint8_t)(pop_item_taken - 1);
|
item_pos = (uint8_t)(pop_item_taken - 1);
|
||||||
item_bake = 2; /* запечь на ОБЕИХ страницах */
|
item_bake = 2; /* запечь на ОБЕИХ страницах */
|
||||||
add_tile_override(cur_room, item_pos, TILE_FLOOR);
|
pop_level_set_tile(cur_room, item_pos, TILE_FLOOR);
|
||||||
pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
|
pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
|
||||||
pop_item_taken = 0;
|
pop_item_taken = 0;
|
||||||
}
|
}
|
||||||
@@ -414,22 +763,39 @@ int main(void)
|
|||||||
* Приоритет как в оригинале: активный flash_time > кадр урона
|
* Приоритет как в оригинале: активный flash_time > кадр урона
|
||||||
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
|
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
|
||||||
if (pop_flash_time) {
|
if (pop_flash_time) {
|
||||||
flash_bg(255, 255, 85); /* color_14_brightyellow */
|
/* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color
|
||||||
|
* оригинала; нам хватает этих двух). */
|
||||||
|
if (pop_flash_red) flash_bg(255, 0, 0); /* color_4_red */
|
||||||
|
else flash_bg(255, 255, 85); /* color_14_brightyellow */
|
||||||
pop_flash_time--;
|
pop_flash_time--;
|
||||||
|
if (!pop_flash_time) pop_flash_red = 0;
|
||||||
flash_on = 1;
|
flash_on = 1;
|
||||||
} else if (pop_kid_hurt) {
|
} else if (pop_kid_hurt) {
|
||||||
flash_bg(255, 85, 85); /* color_12_brightred */
|
flash_bg(255, 85, 85); /* color_12_brightred */
|
||||||
flash_on = 1;
|
flash_on = 1;
|
||||||
}
|
}
|
||||||
io_border = 4; /* зеленый */
|
|
||||||
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
|
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
|
||||||
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
|
PROF(6); /* спрайты: страж, Kid, fore поверх */
|
||||||
|
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
|
||||||
|
* персонажа (см. guard_over_kid). */
|
||||||
|
uint8_t g_after = guard_over_kid();
|
||||||
|
if (!g_after) pop_guard_draw();
|
||||||
|
PROF(6); /* kid_draw (тот же циан) */
|
||||||
|
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
|
||||||
|
if (g_after) pop_guard_draw();
|
||||||
|
}
|
||||||
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
|
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
|
||||||
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
|
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
|
||||||
kid_draw_splash();
|
kid_draw_splash();
|
||||||
pop_kid_hurt = 0;
|
pop_kid_hurt = 0;
|
||||||
}
|
}
|
||||||
io_border = 0; /* черный */
|
pop_hp_draw(); /* полоса HP — только при изменении */
|
||||||
|
PROF(6); /* fore поверх Kid + борта */
|
||||||
|
/* Окно fore-клипа общее, и pop_guard_draw ставит своё. При «страж
|
||||||
|
* поверх» он отрабатывает ПОСЛЕ kid_draw, поэтому окно Кида
|
||||||
|
* восстанавливаем явно — иначе передние грани у Кида отсекаются и
|
||||||
|
* он рисуется поверх столбов. */
|
||||||
|
kid_fore_clip_restore();
|
||||||
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
||||||
kid_fp_width(), kid_fp_height(),
|
kid_fp_width(), kid_fp_height(),
|
||||||
Kid.direction, Kid.frame, Kid.action);
|
Kid.direction, Kid.frame, Kid.action);
|
||||||
@@ -439,7 +805,7 @@ int main(void)
|
|||||||
* сделан (скрытая страница), wait_vsync ловит начало кадра,
|
* сделан (скрытая страница), wait_vsync ловит начало кадра,
|
||||||
* set_visible_page переключает во время бланка (gfx.h паттерн). */
|
* set_visible_page переключает во время бланка (gfx.h паттерн). */
|
||||||
|
|
||||||
io_border = 0; /* черный: ждём vsync */
|
PROF(0); /* чёрный: ждём vsync */
|
||||||
|
|
||||||
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
|
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
|
||||||
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
|
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
|
||||||
@@ -480,10 +846,13 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
|
||||||
pop_ctrl_close();
|
pop_ctrl_close();
|
||||||
closegraph();
|
closegraph();
|
||||||
pop_bg_free();
|
pop_bg_free();
|
||||||
pop_kid_free();
|
pop_kid_free();
|
||||||
|
pop_guard_free();
|
||||||
|
pop_kid_data_free();
|
||||||
pop_level_free();
|
pop_level_free();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
build/
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Модульные тесты движка roomtest под ucsim_z80. Обвязка общая (testkit/),
|
||||||
|
# здесь — только сами наборы и список модулей, которые в них линкуются.
|
||||||
|
TESTKIT := $(abspath $(CURDIR)/../../../../testkit)
|
||||||
|
ENGINE_DIR := $(abspath $(CURDIR)/..)
|
||||||
|
|
||||||
|
# t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
|
||||||
|
# абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве
|
||||||
|
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
|
||||||
|
# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88).
|
||||||
|
CODE_LOC := 0x1000
|
||||||
|
|
||||||
|
# Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/
|
||||||
|
# sprinter: под тестом эти функции заглушены (stubs.c), но объявления
|
||||||
|
# берутся настоящие — иначе разъедутся сигнатуры.
|
||||||
|
PROJ_ROOT := $(abspath $(TESTKIT)/..)
|
||||||
|
EXTRA_INC := -I$(PROJ_ROOT)/libc/include -I$(PROJ_ROOT)/libbgi/include -I$(CURDIR)
|
||||||
|
|
||||||
|
OBJS_geom := build/eng_pop_geom.rel
|
||||||
|
|
||||||
|
OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
|
||||||
|
build/eng_pop_map.rel build/eng_pop_ctrl.rel \
|
||||||
|
build/eng_pop_state.rel \
|
||||||
|
build/stubs.rel build/scene.rel build/kid_blob.rel \
|
||||||
|
build/bank_stub.rel
|
||||||
|
|
||||||
|
# Пробный набор (разведка, без эталонов) линкуется тем же составом.
|
||||||
|
OBJS_grab := $(OBJS_phys)
|
||||||
|
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
|
||||||
|
OBJS_wall := $(OBJS_phys)
|
||||||
|
|
||||||
|
include $(TESTKIT)/host-tests.mk
|
||||||
|
|
||||||
|
# --- t_phys: блоб таблиц анимации ------------------------------------
|
||||||
|
KID_BIN := $(abspath $(CURDIR)/../../poc/res/kid/kid_data.bin)
|
||||||
|
|
||||||
|
build/kid_blob.c: gen_kid_blob.py $(KID_BIN) | $(BUILD)
|
||||||
|
@python3 gen_kid_blob.py $(KID_BIN) $@
|
||||||
|
|
||||||
|
build/kid_blob.rel: build/kid_blob.c kidblob.h | $(BUILD)
|
||||||
|
@$(SDCC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
build/stubs.rel: stubs.c stubs.h kidblob.h | $(BUILD)
|
||||||
|
@$(SDCC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
build/scene.rel: scene.c scene.h stubs.h kidblob.h | $(BUILD)
|
||||||
|
@$(SDCC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
build/bank_stub.rel: bank_stub.s | $(BUILD)
|
||||||
|
@$(SDASZ80) -plosgff $@ $<
|
||||||
|
|
||||||
|
# Эталоны трасс: пересобрать набор, если их правили (см. шапку t_phys.c).
|
||||||
|
build/phys.rel: $(wildcard golden/*.h)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# tests-host — модульные тесты движка roomtest
|
||||||
|
|
||||||
|
Прогоняются под `ucsim_z80`, обвязка общая — `testkit/` (там же объяснение,
|
||||||
|
почему под z80, а не хостовым gcc, и грабли).
|
||||||
|
|
||||||
|
```
|
||||||
|
make host-tests # из корня, вместе с остальными
|
||||||
|
make -C applications/PoP/roomtest/tests-host # только эти
|
||||||
|
make -C applications/PoP/roomtest/tests-host t_geom
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что покрыто
|
||||||
|
|
||||||
|
| набор | модуль | что проверяет |
|
||||||
|
|-------|--------|---------------|
|
||||||
|
| `t_geom` | `pop_geom.c` | геометрия комнаты (`x_bump`, `y_land`, `y_to_row`) и PRNG оригинала |
|
||||||
|
| `t_phys` | `pop_map.c` + `pop_kid.c` + `pop_ctrl.c` | покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал |
|
||||||
|
|
||||||
|
Главный тест в `t_geom` — `geom_lcg_matches_reference`. LCG оригинала
|
||||||
|
(`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме
|
||||||
|
Горнера ради обхода `__mullong`, и заявка «бит-в-бит как в SDLPoP» до
|
||||||
|
появления теста держалась только на комментарии. Тест сверяет рукописный
|
||||||
|
asm с наивной 32-битной формулой на 128 шагах — и по возвращаемому
|
||||||
|
значению, и по обеим половинам сида.
|
||||||
|
|
||||||
|
## t_phys: характеризация, а не проверка правильности
|
||||||
|
|
||||||
|
Набор существует ради **страховки под рефакторинг**. Эталоны сняты с
|
||||||
|
текущего билда, то есть консервируют и текущие баги — правильность
|
||||||
|
по-прежнему проверяется сверкой с `SDLPoP/src/`. Ценность в другом:
|
||||||
|
ближайший крупный шаг (перенос физики с `Kid.` на `Char.`, чтобы её мог
|
||||||
|
переиспользовать страж) обязан быть **поведение-сохраняющим**, и «трасса до
|
||||||
|
== трасса после» ловит ровно тот класс ошибок, который там возможен.
|
||||||
|
|
||||||
|
Поэтому эталон **переснимается только осознанно**, и в коммите должно быть
|
||||||
|
написано, что и почему поменялось. Молча обновлённый эталон обесценивает
|
||||||
|
весь набор.
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 record_golden.py # переснять все трассы
|
||||||
|
python3 record_golden.py 7 9 # только сценарии 7 и 9
|
||||||
|
```
|
||||||
|
|
||||||
|
Трасса — по одной записи на кадр: `frame, x, y, dir, col, row, action` и
|
||||||
|
смещение `curr_seq` от `SEQTBL_BASE`. Кадр гоняется ровно в том порядке,
|
||||||
|
что и в главном цикле `roomtest.c`:
|
||||||
|
|
||||||
|
```
|
||||||
|
pop_ctrl_tick(); // ввод -> control(): смена последовательности
|
||||||
|
kid_tick(); // play_seq: следующий кадр
|
||||||
|
pop_phys_tick(); // падение/приземление/стена
|
||||||
|
pop_loose_tick(); // досчёт тряски и снятие провалившейся плиты
|
||||||
|
```
|
||||||
|
|
||||||
|
Покадровость здесь принципиальна: порт seg005/seg006 — конечный автомат, и
|
||||||
|
ошибка почти всегда проявляется не в одной функции, а в расхождении
|
||||||
|
траектории через несколько кадров. Плюс главный риск переноса на `Char` —
|
||||||
|
не арифметика, а то, **кто владеет окном `Char` внутри кадра**; поймать это
|
||||||
|
можно только прогоном полного кадра.
|
||||||
|
|
||||||
|
Проверка, что харнесс воспроизводит устройство: сценарий `stand` даёт
|
||||||
|
`frame=15, x=114, y=118, curr_seq=0x19A3` — те же значения, что читаются из
|
||||||
|
`_Kid` в живом MAME.
|
||||||
|
|
||||||
|
### Как это заработало под ucsim
|
||||||
|
|
||||||
|
Три шва, каждый закрыт без правок продуктового кода:
|
||||||
|
|
||||||
|
1. **Таблицы анимации.** `pop_kid.c` ходит по ним абсолютными адресами от
|
||||||
|
`KD_DATA_OFF` (0x100) — там, куда на устройстве их приводит маппинг W0.
|
||||||
|
В тесте `kid_data.bin` просто кладётся по 0x100 (`gen_kid_blob.py`), а
|
||||||
|
`gfx_w0_map` заглушен. Следствие: код набора линкуется с
|
||||||
|
`--code-loc 0x1000` (`CODE_LOC` в Makefile), чтобы не налезть на блоб.
|
||||||
|
2. **Окружение.** `stubs.c` — libc/libbgi (графика, память, файлы), соседние
|
||||||
|
модули (`pop_bg`/`pop_trob`/`pop_level`/`pop_redraw`) и состояние стража.
|
||||||
|
Заглушки не пустые там, где это меняет смысл: файловое чтение реально
|
||||||
|
отдаёт `kid_data.bin` (иначе `pop_kid_data_load` тихо сдаётся, `kdat_ok`
|
||||||
|
остаётся нулём и `play_seq` не делает ничего — трасса выходит из одного
|
||||||
|
застывшего кадра), клавиатура отвечает по набору «нажатых» скан-кодов
|
||||||
|
(так под тест попадают `read_input` и `read_user_control`, а не только
|
||||||
|
диспетчер), а обращения к соседям **журналируются** (`tk_log`) — можно
|
||||||
|
проверять «плита отвалилась и попросила перерисовку», а не только
|
||||||
|
координаты.
|
||||||
|
3. **`__banked`.** Модули физики помечены как банковые, SDCC генерирует на
|
||||||
|
них трамплин `___sdcc_bcall_ehl`, которому нужны `set_bank`/`get_bank`.
|
||||||
|
В тесте память плоская — `bank_stub.s` отдаёт нулевой банк и пустое
|
||||||
|
переключение, соблюдая контракт по регистрам.
|
||||||
|
|
||||||
|
Что харнесс НЕ покрывает и остаётся за MAME: отрисовка и окклюзия, реальные
|
||||||
|
переключения банков, тайминги кадра, настоящая клавиатура.
|
||||||
|
|
||||||
|
## Что нужно, чтобы двинуться дальше
|
||||||
|
|
||||||
|
- **Доступ к странице уровня.** `pop_level.c` ходит по абсолютным адресам
|
||||||
|
(`(uint8_t *)(LVL_DATA_OFF + …)` после `gfx_w0_map`). Для `pop_map` это не
|
||||||
|
потребовалось — карту комнаты в него ИНЪЕКТИРУЮТ через `pop_map_set`, —
|
||||||
|
но набор на сам `pop_level`/`pop_trob` в это упрётся. Решение то же, что
|
||||||
|
для `kid_data`: положить страницу по нужному адресу и поднять `CODE_LOC`.
|
||||||
|
- **Сценарии из `bug_closed.md`.** У каждой закрытой записи есть симптом и
|
||||||
|
ожидаемое поведение — готовый список регрессионных кейсов, который стоит
|
||||||
|
переносить в `t_phys` по мере надобности.
|
||||||
|
- **BUG-LOOSE-2** (гонка «уйти из комнаты раньше, чем долетит плита») —
|
||||||
|
через мост MAME воспроизвести не удалось, а на уровне логики это
|
||||||
|
несколько строк: заспавнить кусок, сменить комнату, тикать до
|
||||||
|
приземления, проверить щебень.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
;--------------------------------------------------------------------------
|
||||||
|
; bank_stub.s — set_bank/get_bank для прогона под ucsim.
|
||||||
|
;
|
||||||
|
; Модули движка помечены __banked (в продукте они лежат в страницах W3), и
|
||||||
|
; SDCC генерирует на такие вызовы трамплин ___sdcc_bcall_ehl из своей
|
||||||
|
; библиотеки. Трамплин обязан найти две пользовательские подпрограммы:
|
||||||
|
;
|
||||||
|
; get_bank — вернуть в A номер текущего банка, прочие регистры не трогать
|
||||||
|
; set_bank — переключить банк на A, прочие регистры не трогать
|
||||||
|
;
|
||||||
|
; В тесте банков нет: вся память плоская, все модули слинкованы вместе.
|
||||||
|
; Поэтому «банк» всегда нулевой, а переключение — пустое. Важно только
|
||||||
|
; соблюсти контракт по регистрам, иначе трамплин испортит аргументы вызова.
|
||||||
|
;--------------------------------------------------------------------------
|
||||||
|
.area _CODE
|
||||||
|
|
||||||
|
.globl set_bank
|
||||||
|
.globl get_bank
|
||||||
|
|
||||||
|
get_bank::
|
||||||
|
ld a, #0x00
|
||||||
|
ret
|
||||||
|
|
||||||
|
set_bank::
|
||||||
|
ret
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
gen_kid_blob.py — kid_data.bin как C-массив для тестов под ucsim.
|
||||||
|
|
||||||
|
Зачем. Таблицы кадров и seqtbl живут в EMM-странице, замапленной в W0, и
|
||||||
|
pop_kid ходит по ним АБСОЛЮТНЫМИ адресами от KD_DATA_OFF (0x100). В ucsim
|
||||||
|
никакого W0 нет, зато есть ровная 64-КБ память, поэтому тест просто кладёт
|
||||||
|
файл по 0x100 и глушит gfx_w0_map. Единственное следствие для сборки —
|
||||||
|
код набора линкуется с --code-loc выше конца блоба (см. Makefile).
|
||||||
|
|
||||||
|
Файл берётся ровно тот, что уходит на устройство, — pop_extract_kid_data.py
|
||||||
|
делает его из оригинального PRINCE.EXE, так что тест проверяет порт против
|
||||||
|
НАСТОЯЩИХ данных игры, а не против переписанной от руки таблицы.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
|
||||||
|
data = src.read_bytes()
|
||||||
|
|
||||||
|
out = [
|
||||||
|
"/* Сгенерировано gen_kid_blob.py — не править руками. */",
|
||||||
|
'#include "kidblob.h"',
|
||||||
|
"",
|
||||||
|
f"const unsigned char kid_blob[{len(data)}] = {{",
|
||||||
|
]
|
||||||
|
for i in range(0, len(data), 16):
|
||||||
|
out.append(" " + ",".join(str(b) for b in data[i:i + 16]) + ",")
|
||||||
|
out += [
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
f"const unsigned int kid_blob_size = {len(data)};",
|
||||||
|
"",
|
||||||
|
"/* Разложить блоб по KD_DATA_OFF. Без memcpy: тестовый бинарь не",
|
||||||
|
" * линкуется с libc, а тянуть ради этого z80-рантайм незачем. */",
|
||||||
|
"void kid_blob_install(void)",
|
||||||
|
"{",
|
||||||
|
" unsigned char *d = (unsigned char *)KID_BLOB_ADDR;",
|
||||||
|
" const unsigned char *s = kid_blob;",
|
||||||
|
f" unsigned int n = {len(data)};",
|
||||||
|
" while (n--) *d++ = *s++;",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
dst.write_text("\n".join(out))
|
||||||
|
print(f"gen_kid_blob: {src.name} -> {dst.name} ({len(data)} Б)")
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* Эталон трассы: crouch. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x6B, 0x81, 0x76, 0x00, 0x04, 0x01, 0x01, 0x3E, 0x06,
|
||||||
|
0x6C, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x41, 0x06,
|
||||||
|
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
|
||||||
|
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
|
||||||
|
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
|
||||||
|
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
|
||||||
|
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
|
||||||
|
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
|
||||||
|
0x6E, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4A, 0x06,
|
||||||
|
0x6F, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4B, 0x06,
|
||||||
|
0x70, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4E, 0x06,
|
||||||
|
0x71, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4F, 0x06,
|
||||||
|
0x72, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x52, 0x06,
|
||||||
|
0x73, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x53, 0x06,
|
||||||
|
0x74, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x54, 0x06,
|
||||||
|
0x75, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x57, 0x06,
|
||||||
|
0x76, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x58, 0x06,
|
||||||
|
0x77, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x59, 0x06,
|
||||||
|
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* Эталон трассы: fall_ledge. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
|
||||||
|
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
|
||||||
|
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
|
||||||
|
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
|
||||||
|
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
|
||||||
|
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
|
||||||
|
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
|
||||||
|
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
|
||||||
|
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x66, 0x9E, 0x79, 0x00, 0x06, 0x02, 0x03, 0x77, 0x02,
|
||||||
|
0x67, 0xA0, 0x7F, 0x00, 0x06, 0x02, 0x03, 0x7C, 0x02,
|
||||||
|
0x68, 0x9F, 0x88, 0x00, 0x06, 0x02, 0x03, 0x81, 0x02,
|
||||||
|
0x69, 0x9F, 0x94, 0x00, 0x06, 0x02, 0x03, 0x84, 0x02,
|
||||||
|
0x6A, 0x9E, 0xA6, 0x00, 0x06, 0x02, 0x04, 0xDE, 0x03,
|
||||||
|
0x6B, 0xA0, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC3, 0x06,
|
||||||
|
0x6C, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC6, 0x06,
|
||||||
|
0x6D, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x01, 0xC9, 0x06,
|
||||||
|
0x6E, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4A, 0x06,
|
||||||
|
0x6F, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4B, 0x06,
|
||||||
|
0x70, 0xA5, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4E, 0x06,
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* Эталон трассы: jump_up. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x43, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x12, 0x04,
|
||||||
|
0x44, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x13, 0x04,
|
||||||
|
0x45, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x14, 0x04,
|
||||||
|
0x46, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x15, 0x04,
|
||||||
|
0x47, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x16, 0x04,
|
||||||
|
0x48, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x17, 0x04,
|
||||||
|
0x49, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x18, 0x04,
|
||||||
|
0x4A, 0x80, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x19, 0x04,
|
||||||
|
0x4B, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1A, 0x04,
|
||||||
|
0x4C, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1B, 0x04,
|
||||||
|
0x4D, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1C, 0x04,
|
||||||
|
0x4E, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1D, 0x04,
|
||||||
|
0x4F, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1E, 0x04,
|
||||||
|
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x21, 0x04,
|
||||||
|
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x24, 0x04,
|
||||||
|
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x25, 0x04,
|
||||||
|
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x28, 0x04,
|
||||||
|
0x51, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB8, 0x03,
|
||||||
|
0x52, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB9, 0x03,
|
||||||
|
0x53, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x05, 0xBC, 0x03,
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* Эталон трассы: loose. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
|
||||||
|
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
|
||||||
|
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
|
||||||
|
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
|
||||||
|
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
|
||||||
|
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
|
||||||
|
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
|
||||||
|
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
|
||||||
|
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x9E, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0xA0, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0xA4, 0x76, 0x00, 0x06, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0xA9, 0x76, 0x00, 0x07, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0xAB, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x0E, 0xAE, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2D, 0x00,
|
||||||
|
0x07, 0xB2, 0x76, 0x00, 0x07, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0xB7, 0x76, 0x00, 0x08, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0xB8, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0xBA, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1F, 0x00,
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* Эталон трассы: rjump_gap3. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x01, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x08, 0x00,
|
||||||
|
0x02, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x09, 0x00,
|
||||||
|
0x03, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0A, 0x00,
|
||||||
|
0x04, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0B, 0x00,
|
||||||
|
0x05, 0xB0, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0E, 0x00,
|
||||||
|
0x06, 0xAD, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x11, 0x00,
|
||||||
|
0x07, 0xAA, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0xA5, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0xA4, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0xA2, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x9E, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x99, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0x97, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x22, 0x95, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x64, 0x01,
|
||||||
|
0x23, 0x90, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x67, 0x01,
|
||||||
|
0x24, 0x8A, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x6A, 0x01,
|
||||||
|
0x25, 0x87, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x6D, 0x01,
|
||||||
|
0x26, 0x82, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x72, 0x01,
|
||||||
|
0x27, 0x7B, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x75, 0x01,
|
||||||
|
0x28, 0x6F, 0x73, 0xFF, 0x04, 0x01, 0x01, 0x7A, 0x01,
|
||||||
|
0x29, 0x67, 0x6A, 0xFF, 0x03, 0x01, 0x01, 0x7F, 0x01,
|
||||||
|
0x2A, 0x5F, 0x68, 0xFF, 0x02, 0x01, 0x01, 0x84, 0x01,
|
||||||
|
0x2B, 0x5B, 0x73, 0xFF, 0x01, 0x01, 0x01, 0x89, 0x01,
|
||||||
|
0x2C, 0x57, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x8E, 0x01,
|
||||||
|
0x07, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
|
||||||
|
0x33, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x94, 0x04,
|
||||||
|
0x34, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x95, 0x04,
|
||||||
|
0x0F, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x10, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x28, 0x01,
|
||||||
|
0x11, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x29, 0x01,
|
||||||
|
0x12, 0x50, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2C, 0x01,
|
||||||
|
0x13, 0x4E, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2F, 0x01,
|
||||||
|
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* Эталон трассы: run_right. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
|
||||||
|
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
|
||||||
|
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
|
||||||
|
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
|
||||||
|
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
|
||||||
|
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
|
||||||
|
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
|
||||||
|
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
|
||||||
|
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
|
||||||
|
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
|
||||||
|
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/* Эталон трассы: stand. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* Эталон трассы: step_shift. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x79, 0x72, 0x76, 0x00, 0x03, 0x01, 0x01, 0x6F, 0x05,
|
||||||
|
0x7A, 0x73, 0x76, 0x00, 0x03, 0x01, 0x01, 0x72, 0x05,
|
||||||
|
0x7B, 0x74, 0x76, 0x00, 0x03, 0x01, 0x01, 0x75, 0x05,
|
||||||
|
0x7C, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x78, 0x05,
|
||||||
|
0x7D, 0x7B, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7B, 0x05,
|
||||||
|
0x7E, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7E, 0x05,
|
||||||
|
0x7F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x81, 0x05,
|
||||||
|
0x80, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x82, 0x05,
|
||||||
|
0x81, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x83, 0x05,
|
||||||
|
0x82, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x84, 0x05,
|
||||||
|
0x83, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x85, 0x05,
|
||||||
|
0x84, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x86, 0x05,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
|
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* Эталон трассы: turn. Снят record_golden.py — не править руками.
|
||||||
|
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
|
||||||
|
0x2D, 0x7A, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD1, 0x01,
|
||||||
|
0x2E, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD4, 0x01,
|
||||||
|
0x2F, 0x77, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD7, 0x01,
|
||||||
|
0x30, 0x78, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xDA, 0x01,
|
||||||
|
0x01, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x08, 0x00,
|
||||||
|
0x02, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x09, 0x00,
|
||||||
|
0x03, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0A, 0x00,
|
||||||
|
0x04, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0B, 0x00,
|
||||||
|
0x05, 0x71, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x0E, 0x00,
|
||||||
|
0x06, 0x6E, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x11, 0x00,
|
||||||
|
0x07, 0x6B, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x14, 0x00,
|
||||||
|
0x08, 0x66, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x17, 0x00,
|
||||||
|
0x09, 0x65, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1C, 0x00,
|
||||||
|
0x0A, 0x63, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1F, 0x00,
|
||||||
|
0x0B, 0x5F, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x22, 0x00,
|
||||||
|
0x0C, 0x5A, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x25, 0x00,
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* kidblob.h — таблицы анимации Кида в памяти теста.
|
||||||
|
*
|
||||||
|
* KID_BLOB_ADDR обязан совпадать с KD_DATA_OFF из pop_kid.c: продукт
|
||||||
|
* адресует таблицы именно так, и в этом весь смысл — под тестом работает
|
||||||
|
* тот же код доступа, что на устройстве, просто без маппинга W0.
|
||||||
|
*/
|
||||||
|
#ifndef POP_TESTS_KIDBLOB_H
|
||||||
|
#define POP_TESTS_KIDBLOB_H
|
||||||
|
|
||||||
|
#define KID_BLOB_ADDR 0x0100
|
||||||
|
|
||||||
|
extern const unsigned char kid_blob[];
|
||||||
|
extern const unsigned int kid_blob_size;
|
||||||
|
|
||||||
|
void kid_blob_install(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
record_golden.py — снять эталонные трассы для t_phys.
|
||||||
|
|
||||||
|
Зачем скрипт, а не один прогон. Отчёт tcheck ограничен 768 байтами лога, а
|
||||||
|
трасс на все сценарии сильно больше. Поэтому набор умеет снимать РОВНО
|
||||||
|
ОДИН сценарий за прогон (-DSC_RECORD=<n>), а перебор делает этот скрипт:
|
||||||
|
собирает набор N раз, каждый раз забирает из отчёта имя сценария и его
|
||||||
|
трассу и пишет golden/<имя>.h.
|
||||||
|
|
||||||
|
Эталон фиксирует ТЕКУЩЕЕ поведение. Это осознанно: набор страхует
|
||||||
|
поведение-сохраняющие правки (перенос физики Kid -> Char), а не доказывает
|
||||||
|
правильность. Правильность — сверка с SDLPoP. Поэтому переснимать эталоны
|
||||||
|
можно только вместе с внятным объяснением, что и почему поменялось.
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
python3 record_golden.py # снять все сценарии
|
||||||
|
python3 record_golden.py 7 9 # только эти номера
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
GOLDEN = HERE / "golden"
|
||||||
|
# Число сценариев = число TC_RUN в t_phys.c: список ведётся там, здесь его
|
||||||
|
# дублировать незачем.
|
||||||
|
NSCEN = len(re.findall(r"^\s*TC_RUN\(", (HERE / "t_phys.c").read_text(), re.M))
|
||||||
|
|
||||||
|
|
||||||
|
def record(n: int) -> str:
|
||||||
|
# make не видит смены -DSC_RECORD (в зависимостях только файлы), поэтому
|
||||||
|
# объектник набора сносим руками — иначе следующий прогон молча снимет
|
||||||
|
# трассу предыдущего сценария под новым именем.
|
||||||
|
for f in ("phys.rel", "phys.ihx"):
|
||||||
|
(HERE / "build" / f).unlink(missing_ok=True)
|
||||||
|
subprocess.run(["make", "-s", "build/phys.ihx", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
|
||||||
|
cwd=HERE, check=True)
|
||||||
|
p = subprocess.run(["make", "-s", "run-phys", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
|
||||||
|
cwd=HERE, capture_output=True, text=True)
|
||||||
|
out = p.stdout
|
||||||
|
m = re.search(r"^\s*### (\S+)\s*$", out, re.M)
|
||||||
|
if not m:
|
||||||
|
raise SystemExit(f"сценарий {n}: в отчёте нет строки '### имя'\n{out}")
|
||||||
|
name = m.group(1)
|
||||||
|
rows = re.findall(r"^\s*([0-9A-F]{18})\s*$", out, re.M)
|
||||||
|
if not rows:
|
||||||
|
raise SystemExit(f"сценарий {n} ({name}): трасса пустая\n{out}")
|
||||||
|
|
||||||
|
lines = [f"/* Эталон трассы: {name}. Снят record_golden.py — не править руками.",
|
||||||
|
" * Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */"]
|
||||||
|
for r in rows:
|
||||||
|
lines.append(" " + ", ".join(f"0x{r[i:i+2]}" for i in range(0, 18, 2)) + ",")
|
||||||
|
GOLDEN.mkdir(exist_ok=True)
|
||||||
|
(GOLDEN / f"{name}.h").write_text("\n".join(lines) + "\n")
|
||||||
|
return f"{name}: {len(rows)} кадров"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
nums = [int(a) for a in sys.argv[1:]] or list(range(1, NSCEN + 1))
|
||||||
|
for n in nums:
|
||||||
|
print(f"[{n}/{NSCEN}] " + record(n))
|
||||||
|
# Итоговая пересборка в обычном режиме, чтобы каталог не остался с
|
||||||
|
# объектниками, собранными под SC_RECORD.
|
||||||
|
subprocess.run(["make", "-s", "clean"], cwd=HERE, check=True)
|
||||||
|
print("эталоны сняты; проверить: make -C", HERE.name, "t_phys")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* scene.c — реализация покадрового раннера (см. scene.h).
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <kbd_raw.h>
|
||||||
|
|
||||||
|
#include "tcheck.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
#include "pop_ctrl.h"
|
||||||
|
#include "pop_geom.h"
|
||||||
|
#include "pop_guard.h"
|
||||||
|
#include "kid_data.h"
|
||||||
|
#include "scene.h"
|
||||||
|
#include "stubs.h"
|
||||||
|
#include "kidblob.h"
|
||||||
|
|
||||||
|
sc_rec sc_trace[SC_TRACE_MAX];
|
||||||
|
uint8_t sc_len;
|
||||||
|
|
||||||
|
static uint8_t sc_fg[30];
|
||||||
|
/* Кромки шва: соседей у сцены нет, поэтому по краям стена (тайл 20). */
|
||||||
|
static const uint8_t sc_edge[6] = { 20, 20, 20, 20, 20, 20 };
|
||||||
|
|
||||||
|
void sc_init(void)
|
||||||
|
{
|
||||||
|
kid_blob_install(); /* таблицы туда, куда смотрит pop_kid */
|
||||||
|
pop_kid_data_load("KID\\KID_DATA.BIN");
|
||||||
|
pop_kid_hp_newgame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sc_room(const uint8_t *fg, uint8_t room)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < 30; i++) { sc_fg[i] = fg[i]; tk_modif[i] = 0; }
|
||||||
|
pop_map_set(sc_fg);
|
||||||
|
pop_map_set_room(room);
|
||||||
|
/* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон
|
||||||
|
* и сверху/снизу — тогда решения физики зависят только от sc_fg, и
|
||||||
|
* трасса не поедет, если когда-нибудь поменяется загрузка кромок. */
|
||||||
|
pop_map_set_edges(0, 0, 0, 0, sc_edge, sc_edge);
|
||||||
|
tk_start_room = room;
|
||||||
|
tk_log_clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir)
|
||||||
|
{
|
||||||
|
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
|
||||||
|
pop_kid_hp_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir)
|
||||||
|
{
|
||||||
|
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
|
||||||
|
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */
|
||||||
|
pop_determine_col(); /* колонку пересчитать под новый X */
|
||||||
|
pop_kid_hp_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sc_trace_clear(void) { sc_len = 0; }
|
||||||
|
|
||||||
|
static void sc_record(void)
|
||||||
|
{
|
||||||
|
sc_rec *r;
|
||||||
|
if (sc_len >= SC_TRACE_MAX) return;
|
||||||
|
r = &sc_trace[sc_len++];
|
||||||
|
r->frame = Kid.frame;
|
||||||
|
r->x = Kid.x;
|
||||||
|
r->y = Kid.y;
|
||||||
|
r->dir = Kid.direction;
|
||||||
|
r->col = Kid.curr_col;
|
||||||
|
r->row = Kid.curr_row;
|
||||||
|
r->action = Kid.action;
|
||||||
|
r->seq = (uint16_t)(Kid.curr_seq - SEQTBL_BASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t sc_run(uint8_t keys, uint8_t n)
|
||||||
|
{
|
||||||
|
uint8_t i, done = 0;
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
tk_keys_clear();
|
||||||
|
if (keys & SC_L) tk_key_down(KBD_LEFT);
|
||||||
|
if (keys & SC_R) tk_key_down(KBD_RIGHT);
|
||||||
|
if (keys & SC_U) tk_key_down(KBD_UP);
|
||||||
|
if (keys & SC_D) tk_key_down(KBD_DOWN);
|
||||||
|
if (keys & SC_SHIFT) tk_key_down(KBD_LSHIFT);
|
||||||
|
|
||||||
|
/* Порядок ровно как в главном цикле roomtest.c (kid_heal и
|
||||||
|
* отрисовка выпадают — они ничего не решают). pop_loose_tick
|
||||||
|
* оставлен: он не рисование, а МЕХАНИКА — досчитывает тряску и
|
||||||
|
* убирает провалившуюся плиту из карты, то есть меняет то, по чему
|
||||||
|
* физика в следующем кадре принимает решения. */
|
||||||
|
pop_ctrl_tick();
|
||||||
|
kid_tick();
|
||||||
|
pop_phys_tick();
|
||||||
|
pop_loose_tick();
|
||||||
|
|
||||||
|
sc_record();
|
||||||
|
done++;
|
||||||
|
if (sc_len >= SC_TRACE_MAX) break;
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- сверка с эталоном ---------------------------------------------- */
|
||||||
|
|
||||||
|
void sc_expect(const uint8_t *golden, uint8_t nrec)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
const uint8_t *g = golden;
|
||||||
|
tc_check((int16_t)sc_len, (int16_t)nrec, "sc_len", __LINE__);
|
||||||
|
for (i = 0; i < nrec && i < sc_len; i++, g += SC_REC_BYTES) {
|
||||||
|
const sc_rec *r = &sc_trace[i];
|
||||||
|
/* Номер кадра идёт первым полем каждой проверки: в отчёте видно,
|
||||||
|
* на каком именно кадре трасса разошлась. */
|
||||||
|
tc_check((int16_t)r->frame, (int16_t)g[0], "frame", i);
|
||||||
|
tc_check((int16_t)r->x, (int16_t)g[1], "x", i);
|
||||||
|
tc_check((int16_t)r->y, (int16_t)g[2], "y", i);
|
||||||
|
tc_check((int16_t)r->dir, (int16_t)(int8_t)g[3], "dir", i);
|
||||||
|
tc_check((int16_t)r->col, (int16_t)(int8_t)g[4], "col", i);
|
||||||
|
tc_check((int16_t)r->row, (int16_t)(int8_t)g[5], "row", i);
|
||||||
|
tc_check((int16_t)r->action, (int16_t)g[6], "action", i);
|
||||||
|
tc_check((int16_t)r->seq,
|
||||||
|
(int16_t)((uint16_t)g[7] | ((uint16_t)g[8] << 8)), "seq", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- снятие эталона -------------------------------------------------- */
|
||||||
|
|
||||||
|
static void put(char c)
|
||||||
|
{
|
||||||
|
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put_byte(uint8_t v)
|
||||||
|
{
|
||||||
|
static const char hex[] = "0123456789ABCDEF";
|
||||||
|
put(hex[v >> 4]); put(hex[v & 15]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sc_dump(const char *name)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
tc_result.magic = TC_MAGIC;
|
||||||
|
tc_result.total = 1;
|
||||||
|
tc_result.failed = 1; /* иначе раннер лог не напечатает */
|
||||||
|
put('#'); put('#'); put('#'); put(' ');
|
||||||
|
while (*name) put(*name++);
|
||||||
|
put('\n');
|
||||||
|
for (i = 0; i < sc_len; i++) {
|
||||||
|
const sc_rec *r = &sc_trace[i];
|
||||||
|
put_byte(r->frame);
|
||||||
|
put_byte(r->x);
|
||||||
|
put_byte(r->y);
|
||||||
|
put_byte((uint8_t)r->dir);
|
||||||
|
put_byte((uint8_t)r->col);
|
||||||
|
put_byte((uint8_t)r->row);
|
||||||
|
put_byte(r->action);
|
||||||
|
put_byte((uint8_t)r->seq);
|
||||||
|
put_byte((uint8_t)(r->seq >> 8));
|
||||||
|
put('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* scene.h — покадровый сценарный раннер для тестов физики Кида.
|
||||||
|
*
|
||||||
|
* Почему покадровый, а не «вызвать функцию и сверить возврат». Порт
|
||||||
|
* seg005/seg006 — это конечный автомат: диспетчер выбирает
|
||||||
|
* последовательность, интерпретатор проигрывает её кадр за кадром, физика
|
||||||
|
* между кадрами двигает персонажа и решает, не пора ли падать. Ошибка
|
||||||
|
* почти всегда проявляется не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ
|
||||||
|
* через несколько кадров. Плюс главный риск ближайшего рефакторинга
|
||||||
|
* (Kid -> Char) — не арифметика, а то, КТО владеет окном Char внутри кадра;
|
||||||
|
* поймать это можно только прогоном полного кадра, как в главном цикле:
|
||||||
|
*
|
||||||
|
* pop_ctrl_tick(); // ввод -> control(): смена последовательности
|
||||||
|
* kid_tick(); // play_seq: следующий кадр
|
||||||
|
* pop_phys_tick(); // падение/приземление/стена
|
||||||
|
*
|
||||||
|
* Отсюда формат: сцена (комната + стартовая поза) + скрипт ввода, на
|
||||||
|
* выходе — трасса состояния Кида по кадрам. Сравнение трассы с эталоном
|
||||||
|
* и есть тест.
|
||||||
|
*/
|
||||||
|
#ifndef POP_TESTS_SCENE_H
|
||||||
|
#define POP_TESTS_SCENE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Одна запись трассы: то, чем оперируют решения движка. curr_seq —
|
||||||
|
* СМЕЩЕНИЕ от SEQTBL_BASE (адрес зависит от базы, смещение — нет). */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t frame;
|
||||||
|
uint8_t x, y;
|
||||||
|
int8_t dir;
|
||||||
|
int8_t col, row;
|
||||||
|
uint8_t action;
|
||||||
|
uint16_t seq;
|
||||||
|
} sc_rec;
|
||||||
|
|
||||||
|
#define SC_TRACE_MAX 48
|
||||||
|
extern sc_rec sc_trace[SC_TRACE_MAX];
|
||||||
|
extern uint8_t sc_len;
|
||||||
|
|
||||||
|
/* Кнопки скрипта ввода — битовая маска на кадр. */
|
||||||
|
#define SC_L 0x01
|
||||||
|
#define SC_R 0x02
|
||||||
|
#define SC_U 0x04
|
||||||
|
#define SC_D 0x08
|
||||||
|
#define SC_SHIFT 0x10
|
||||||
|
|
||||||
|
/* Инициализация окружения набора: таблицы анимации, обнуление состояния.
|
||||||
|
* Звать один раз в начале прогона. */
|
||||||
|
void sc_init(void);
|
||||||
|
|
||||||
|
/* Задать комнату: fg[30] — коды тайлов по рядам (row*10+col), room —
|
||||||
|
* номер (нужен trob'ам). Модификаторы обнуляются; ставить их тест может
|
||||||
|
* сам через tk_modif. Соседи комнаты — стены (изолированная сцена). */
|
||||||
|
void sc_room(const uint8_t *fg, uint8_t room);
|
||||||
|
|
||||||
|
/* Поставить Кида: колонка/ряд задают позицию по центру тайла, как это
|
||||||
|
* делает do_startpos (seg003) — так сцена описывается в терминах карты, а
|
||||||
|
* не в пикселях. */
|
||||||
|
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir);
|
||||||
|
|
||||||
|
/* То же, но с точным X: тайл задаёт ряд и пол под ногами, X — фазу внутри
|
||||||
|
* тайла (нужно, когда исход зависит от того, где именно Кид стоит). */
|
||||||
|
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir);
|
||||||
|
|
||||||
|
/* Прогнать n кадров с ЗАЖАТОЙ маской keys, записывая трассу.
|
||||||
|
* Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */
|
||||||
|
uint8_t sc_run(uint8_t keys, uint8_t n);
|
||||||
|
|
||||||
|
/* Сбросить трассу (перед новым сценарием). */
|
||||||
|
void sc_trace_clear(void);
|
||||||
|
|
||||||
|
/* Сверить трассу с эталоном: golden — плоский массив по 8 байт на кадр
|
||||||
|
* (frame,x,y,dir,col,row,action,seq_lo,seq_hi — 9), nrec записей.
|
||||||
|
* Расхождения уходят в отчёт tcheck с номером кадра и поля. */
|
||||||
|
void sc_expect(const uint8_t *golden, uint8_t nrec);
|
||||||
|
|
||||||
|
#define SC_REC_BYTES 9
|
||||||
|
|
||||||
|
/* Снятие эталона. Лог tcheck всего 768 байт — на все сценарии сразу трасс
|
||||||
|
* не хватит, поэтому за прогон снимается РОВНО ОДИН сценарий (его номер
|
||||||
|
* задаёт -DSC_RECORD=<n>), а перебором занимается record_golden.py.
|
||||||
|
* Формат: строка «### имя», дальше по строке на кадр, по два хекс-символа
|
||||||
|
* на байт. Раннер печатает лог только при провале — sc_dump ставит
|
||||||
|
* failed=1 намеренно. */
|
||||||
|
void sc_dump(const char *name);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* stubs.c — окружение движка для прогона под ucsim_z80.
|
||||||
|
*
|
||||||
|
* Под тестом крутится НАСТОЯЩАЯ физика (pop_map) с настоящим
|
||||||
|
* интерпретатором последовательностей (pop_kid): это единственный способ
|
||||||
|
* проверить порт seg005/seg006, потому что вся его суть — в точных
|
||||||
|
* величинах кадров и seqtbl. А вот всё, что вокруг, в ucsim не существует:
|
||||||
|
* видеопамять, EMM-страницы, файлы, клавиатура. Здесь они заглушены.
|
||||||
|
*
|
||||||
|
* Три категории:
|
||||||
|
* 1. libc/libbgi (графика, память, файлы, клавиатура) — пустышки. Код
|
||||||
|
* отрисовки из pop_kid.c линкуется, но не вызывается.
|
||||||
|
* 2. Соседние модули движка (pop_bg/pop_trob/pop_level/pop_redraw) —
|
||||||
|
* минимальные модели: room_modif живёт здесь массивом, пометки
|
||||||
|
* перерисовки и падающие куски ЖУРНАЛИРУЮТСЯ (тест может проверить,
|
||||||
|
* что физика их дёрнула), уровень отдаёт стартовую комнату.
|
||||||
|
* 3. Состояние стража — просто данные: страж в тестах физики не тикает,
|
||||||
|
* но pop_map/pop_ctrl на его поля смотрят.
|
||||||
|
*
|
||||||
|
* Клавиатура сделана НЕ пустышкой: `tk_keys` — набор «нажатых» скан-кодов,
|
||||||
|
* и `kbd_raw_down` отвечает по нему. Так под тест попадают и read_input, и
|
||||||
|
* read_user_control, то есть весь путь ввода, а не только диспетчер.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprinter_mem.h>
|
||||||
|
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_guard.h"
|
||||||
|
#include "pop_bg.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
#include "pop_trob.h"
|
||||||
|
#include "pop_level.h"
|
||||||
|
#include "pop_redraw.h"
|
||||||
|
#include "_pop_draw.h"
|
||||||
|
#include "stubs.h"
|
||||||
|
#include "kidblob.h"
|
||||||
|
|
||||||
|
/* ---- 1. libc / libbgi ---------------------------------------------- */
|
||||||
|
|
||||||
|
uint8_t _gfx_visible_page, _gfx_draw_page, _gfx_bank;
|
||||||
|
/* ISR-стаб W0-страницы: продукт берёт его АДРЕС и вписывает в страницу.
|
||||||
|
* Здесь достаточно, чтобы символ существовал. */
|
||||||
|
void _gfx_w0_isr(void) { }
|
||||||
|
|
||||||
|
void gfx_w0_map(uint8_t page) { (void)page; }
|
||||||
|
void gfx_w0_unmap(void) { }
|
||||||
|
|
||||||
|
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
|
||||||
|
{ (void)x; (void)y; (void)img; (void)flip; }
|
||||||
|
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
||||||
|
int skip, int rows)
|
||||||
|
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
|
||||||
|
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
|
||||||
|
int skip, int rows, uint8_t maxw)
|
||||||
|
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; (void)maxw; }
|
||||||
|
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
|
||||||
|
uint8_t skip, uint8_t rows)
|
||||||
|
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
|
||||||
|
|
||||||
|
int atlas_load(atlas_t *a, const char *path) { (void)a; (void)path; return -1; }
|
||||||
|
void atlas_free(atlas_t *a) { (void)a; }
|
||||||
|
const void *atlas_image(const atlas_t *a, uint8_t idx)
|
||||||
|
{ (void)a; (void)idx; return 0; }
|
||||||
|
|
||||||
|
/* EMM: страниц нет, но блок обязан выделиться — иначе pop_kid_data_load
|
||||||
|
* сдастся на первом же шаге и молча оставит kdat_ok = 0, а play_seq тогда
|
||||||
|
* просто ничего не делает (и трасса выходит из одного застывшего кадра). */
|
||||||
|
uint8_t mem_alloc_pages_bios(uint8_t n) { (void)n; return 1; }
|
||||||
|
void mem_free_block_bios(uint8_t blk) { (void)blk; }
|
||||||
|
uint8_t mem_get_page_bios(uint8_t blk, uint8_t idx) { (void)blk; (void)idx; return 1; }
|
||||||
|
|
||||||
|
/* Файловая система: единственный «файл» — таблицы анимации. Сделано не
|
||||||
|
* пустышкой, а рабочим чтением, чтобы pop_kid_data_load отработал целиком
|
||||||
|
* (включая правку ISR-стаба страницы) и сам взвёл свой kdat_ok — тест не
|
||||||
|
* лезет во внутренние статики продукта. Данные, по которым потом ходят
|
||||||
|
* аксессоры, кладёт kid_blob_install по KID_BLOB_ADDR: на устройстве туда
|
||||||
|
* же их приводит маппинг W0. */
|
||||||
|
int open(const char *p, int f) { (void)p; (void)f; return 3; }
|
||||||
|
|
||||||
|
int read(int fd, void *b, int n)
|
||||||
|
{
|
||||||
|
unsigned char *d = (unsigned char *)b;
|
||||||
|
const unsigned char *s = kid_blob;
|
||||||
|
unsigned int i, cnt = kid_blob_size;
|
||||||
|
if (fd != 3) return -1;
|
||||||
|
if ((unsigned int)n < cnt) cnt = (unsigned int)n;
|
||||||
|
for (i = 0; i < cnt; i++) d[i] = s[i];
|
||||||
|
return (int)cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int close(int fd) { (void)fd; return 0; }
|
||||||
|
|
||||||
|
/* ---- клавиатура: управляемая тестом --------------------------------- */
|
||||||
|
|
||||||
|
uint16_t tk_keys[TK_KEYS_MAX];
|
||||||
|
|
||||||
|
int kbd_raw_open(void) { return 0; }
|
||||||
|
void kbd_raw_close(void) { }
|
||||||
|
void kbd_raw_sync(void) { }
|
||||||
|
|
||||||
|
uint8_t kbd_raw_down(uint16_t code)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < TK_KEYS_MAX; i++)
|
||||||
|
if (tk_keys[i] == code) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tk_keys_clear(void)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < TK_KEYS_MAX; i++) tk_keys[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tk_key_down(uint16_t code)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 0; i < TK_KEYS_MAX; i++)
|
||||||
|
if (tk_keys[i] == 0) { tk_keys[i] = code; return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 2. соседние модули движка -------------------------------------- */
|
||||||
|
|
||||||
|
/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота),
|
||||||
|
* зелья (тип). Физика читает и пишет его напрямую через pop_trob_modif. */
|
||||||
|
uint8_t tk_modif[30];
|
||||||
|
uint8_t tk_start_room;
|
||||||
|
|
||||||
|
uint8_t *pop_trob_modif(uint8_t room) __banked { (void)room; return tk_modif; }
|
||||||
|
uint8_t pop_level_start_room(void) { return tk_start_room; }
|
||||||
|
|
||||||
|
/* Журнал: что физика попросила у соседей. Тест сверяет ФАКТ и параметры
|
||||||
|
* (какой тайл помечен, где заспавнен кусок) — заглушка-пустышка этого не
|
||||||
|
* дала бы, а именно такие баги (BUG-GATE-ANIM-1) мы и ловим. */
|
||||||
|
tk_log_t tk_log;
|
||||||
|
|
||||||
|
static void log_put(uint8_t kind, uint8_t a, uint8_t b, uint8_t c)
|
||||||
|
{
|
||||||
|
if (tk_log.n >= TK_LOG_MAX) { tk_log.overflow = 1; return; }
|
||||||
|
tk_log.e[tk_log.n].kind = kind;
|
||||||
|
tk_log.e[tk_log.n].a = a;
|
||||||
|
tk_log.e[tk_log.n].b = b;
|
||||||
|
tk_log.e[tk_log.n].c = c;
|
||||||
|
tk_log.n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tk_log_clear(void) { tk_log.n = 0; tk_log.overflow = 0; }
|
||||||
|
|
||||||
|
uint8_t tk_log_count(uint8_t kind)
|
||||||
|
{
|
||||||
|
uint8_t i, c = 0;
|
||||||
|
for (i = 0; i < tk_log.n; i++) if (tk_log.e[i].kind == kind) c++;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
|
||||||
|
{ log_put(TK_TROB, room, tilepos, (uint8_t)type); }
|
||||||
|
|
||||||
|
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
||||||
|
uint8_t button_type, uint8_t modifier) __banked
|
||||||
|
{ (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); }
|
||||||
|
|
||||||
|
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
|
||||||
|
{ log_put(TK_REDRAW, tilepos, kind, pages); }
|
||||||
|
|
||||||
|
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
|
||||||
|
{ log_put(TK_REDRAW_ABOVE, col, kind, pages); }
|
||||||
|
|
||||||
|
void pop_loose_mob_spawn(int row, int col) __banked
|
||||||
|
{ log_put(TK_MOB_SPAWN, (uint8_t)row, (uint8_t)col, 0); }
|
||||||
|
|
||||||
|
void pop_loose_mob_tick(void) __banked { }
|
||||||
|
void pop_loose_mob_room_changed(void) __banked { }
|
||||||
|
|
||||||
|
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
|
||||||
|
{ (void)col; (void)y; return 0; }
|
||||||
|
|
||||||
|
/* Отрисовочные хвосты, которые физика дёргает напрямую. */
|
||||||
|
void pop_fore_set_clip(int x, int y, int w, int h) __banked
|
||||||
|
{ (void)x; (void)y; (void)w; (void)h; }
|
||||||
|
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
|
||||||
|
{ (void)x; (void)w; (void)sy_top; (void)h; }
|
||||||
|
void pop_heal_fast(int x, int y, int w, int h)
|
||||||
|
{ (void)x; (void)y; (void)w; (void)h; }
|
||||||
|
|
||||||
|
/* ---- 3. состояние стража -------------------------------------------- */
|
||||||
|
|
||||||
|
pop_char_t Guard;
|
||||||
|
kframe pop_gframe;
|
||||||
|
int8_t can_guard_see_kid;
|
||||||
|
int8_t guard_refrac, kid_sword_strike;
|
||||||
|
int8_t offguard, holding_sword;
|
||||||
|
/* «Кид нашумел» — физика взводит его на ударе о стену, приземлении и
|
||||||
|
* обрушенной плите (BUG-GUARD-DEAF-1); читает только автопилот стража,
|
||||||
|
* которого в наборе нет. */
|
||||||
|
int8_t is_guard_notice;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* stubs.h — интерфейс тестового окружения (см. stubs.c).
|
||||||
|
*
|
||||||
|
* Что тест может задавать: «нажатые» клавиши (tk_key_down) и модификаторы
|
||||||
|
* тайлов комнаты (tk_modif). Что может проверять: журнал обращений
|
||||||
|
* физики к соседним модулям (tk_log).
|
||||||
|
*/
|
||||||
|
#ifndef POP_TESTS_STUBS_H
|
||||||
|
#define POP_TESTS_STUBS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* ---- клавиатура ------------------------------------------------------ */
|
||||||
|
#define TK_KEYS_MAX 6
|
||||||
|
extern uint16_t tk_keys[TK_KEYS_MAX];
|
||||||
|
void tk_keys_clear(void);
|
||||||
|
void tk_key_down(uint16_t code);
|
||||||
|
|
||||||
|
/* ---- комната --------------------------------------------------------- */
|
||||||
|
extern uint8_t tk_modif[30]; /* room_modif текущей комнаты */
|
||||||
|
extern uint8_t tk_start_room; /* что вернёт pop_level_start_room */
|
||||||
|
|
||||||
|
/* ---- журнал обращений ------------------------------------------------ */
|
||||||
|
#define TK_LOG_MAX 24
|
||||||
|
|
||||||
|
enum {
|
||||||
|
TK_TROB = 1, /* a=room b=tilepos c=type */
|
||||||
|
TK_BUTTON, /* a=room b=tilepos c=type */
|
||||||
|
TK_REDRAW, /* a=tilepos b=kind c=pages */
|
||||||
|
TK_REDRAW_ABOVE, /* a=col b=kind c=pages */
|
||||||
|
TK_MOB_SPAWN /* a=row b=col */
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct { uint8_t kind, a, b, c; } tk_log_e;
|
||||||
|
typedef struct { uint8_t n, overflow; tk_log_e e[TK_LOG_MAX]; } tk_log_t;
|
||||||
|
|
||||||
|
extern tk_log_t tk_log;
|
||||||
|
void tk_log_clear(void);
|
||||||
|
|
||||||
|
/* Сколько записей данного вида в журнале (0 = не было). */
|
||||||
|
uint8_t tk_log_count(uint8_t kind);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* t_geom.c — тесты pop_geom.c (геометрия комнаты + PRNG оригинала).
|
||||||
|
*
|
||||||
|
* Модуль выбран первым, потому что не тянет за собой ничего: ни графики,
|
||||||
|
* ни данных уровня, ни клавиатуры — только свой заголовок.
|
||||||
|
*
|
||||||
|
* Главный тест здесь — geom_lcg_matches_reference. В pop_geom.c LCG
|
||||||
|
* оригинала (s = s*214013 + 2531011) написан на ассемблере по схеме
|
||||||
|
* Горнера ради обхода __mullong, и заявка «бит-в-бит как в SDLPoP» до сих
|
||||||
|
* пор держалась только на комментарии. Тест сверяет рукописный asm с
|
||||||
|
* наивной 32-битной формулой на каждом шаге — и по возвращаемому значению,
|
||||||
|
* и по обеим половинам сида.
|
||||||
|
*/
|
||||||
|
#include "tcheck.h"
|
||||||
|
#include "pop_geom.h"
|
||||||
|
|
||||||
|
/* ---- геометрия ------------------------------------------------------ */
|
||||||
|
|
||||||
|
TC_TEST(geom_xbump_layout)
|
||||||
|
{
|
||||||
|
/* Шаг колонок равномерный и равен TILE_SIZEX — на этом стоит вся
|
||||||
|
* арифметика колонок (x_bump[col + FIRST_ONSCREEN_COLUMN]). */
|
||||||
|
uint8_t i;
|
||||||
|
for (i = 1; i < 20; i++)
|
||||||
|
TC_EQ(pop_x_bump[i] - pop_x_bump[i - 1], TILE_SIZEX);
|
||||||
|
|
||||||
|
/* Центр тайла колонки 0 — та самая величина, которую кладут в Char.x
|
||||||
|
* do_startpos и pos_guards (seg003). */
|
||||||
|
TC_EQ(pop_x_bump[0 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 72);
|
||||||
|
TC_EQ(pop_x_bump[9 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 198);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(geom_yland_rows)
|
||||||
|
{
|
||||||
|
/* y_land[row + 1]; [0] — ряд «над комнатой». */
|
||||||
|
TC_EQ(pop_y_land[0], -8);
|
||||||
|
TC_EQ(pop_y_land[1], 55);
|
||||||
|
TC_EQ(pop_y_land[2], 118);
|
||||||
|
TC_EQ(pop_y_land[3], 181);
|
||||||
|
TC_EQ(pop_y_land[4], 244);
|
||||||
|
/* Шаг между рядами — TILE_SIZEY. */
|
||||||
|
TC_EQ(pop_y_land[2] - pop_y_land[1], TILE_SIZEY);
|
||||||
|
TC_EQ(pop_y_land[3] - pop_y_land[2], TILE_SIZEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(geom_y_to_row)
|
||||||
|
{
|
||||||
|
/* Пол каждого ряда должен давать номер этого ряда. */
|
||||||
|
TC_EQ(pop_y_to_row(pop_y_land[1]), 0);
|
||||||
|
TC_EQ(pop_y_to_row(pop_y_land[2]), 1);
|
||||||
|
TC_EQ(pop_y_to_row(pop_y_land[3]), 2);
|
||||||
|
/* Ряд над комнатой. */
|
||||||
|
TC_EQ(pop_y_to_row(pop_y_land[0]), -1);
|
||||||
|
/* Оборот mod 4: ряд «под комнатой» сворачивается обратно в -1 —
|
||||||
|
* ровно на это опирается спавн падающего куска у нижней кромки. */
|
||||||
|
TC_EQ(pop_y_to_row(pop_y_land[4]), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- PRNG: сверка asm-LCG с эталонной формулой ---------------------- */
|
||||||
|
|
||||||
|
#if POP_PRANDOM_EXACT
|
||||||
|
|
||||||
|
#define LCG_ITERS 128
|
||||||
|
|
||||||
|
TC_TEST(geom_lcg_matches_reference)
|
||||||
|
{
|
||||||
|
pop_rnd_t s;
|
||||||
|
unsigned long ref = 12345UL; /* произвольный ненулевой старт */
|
||||||
|
uint16_t i, got, want;
|
||||||
|
|
||||||
|
pop_prandom_set(s, 12345u);
|
||||||
|
|
||||||
|
for (i = 0; i < LCG_ITERS; i++) {
|
||||||
|
got = pop_prandom(&s, 255u);
|
||||||
|
ref = ref * 214013UL + 2531011UL;
|
||||||
|
want = (uint16_t)((uint16_t)(ref >> 16) % 256u);
|
||||||
|
|
||||||
|
/* Обрываемся на первом расхождении: иначе одна ошибка в умножении
|
||||||
|
* забила бы весь буфер отчёта однотипными строками. */
|
||||||
|
if (got != want ||
|
||||||
|
s.lo != (uint16_t)ref ||
|
||||||
|
s.hi != (uint16_t)(ref >> 16)) {
|
||||||
|
TC_EQ(got, want);
|
||||||
|
TC_EQ(s.lo, (uint16_t)ref);
|
||||||
|
TC_EQ(s.hi, (uint16_t)(ref >> 16));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TC_EQ(i, LCG_ITERS); /* прошли всю дистанцию без расхождений */
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(geom_lcg_seed_zero)
|
||||||
|
{
|
||||||
|
/* Ноль — законный сид (кладку сеют номером комнаты + ряд + колонка,
|
||||||
|
* что вполне даёт 0). У LCG ноль не является неподвижной точкой —
|
||||||
|
* убеждаемся, что asm это воспроизводит. */
|
||||||
|
pop_rnd_t s;
|
||||||
|
unsigned long ref = 0UL;
|
||||||
|
pop_prandom_set(s, 0u);
|
||||||
|
pop_prandom(&s, 255u);
|
||||||
|
ref = ref * 214013UL + 2531011UL;
|
||||||
|
TC_EQ(s.lo, (uint16_t)ref);
|
||||||
|
TC_EQ(s.hi, (uint16_t)(ref >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* POP_PRANDOM_EXACT */
|
||||||
|
|
||||||
|
TC_TEST(geom_prandom_range)
|
||||||
|
{
|
||||||
|
/* Оба генератора обязаны укладываться в 0..maxv, в том числе когда
|
||||||
|
* maxv+1 НЕ степень двойки (там другая ветка pop_rnd_fit). */
|
||||||
|
pop_rnd_t s;
|
||||||
|
uint16_t i, v, seen_hi = 0;
|
||||||
|
|
||||||
|
uint16_t out_of_range = 0;
|
||||||
|
|
||||||
|
pop_prandom_set(s, 1u);
|
||||||
|
for (i = 0; i < 200; i++) {
|
||||||
|
v = pop_prandom(&s, 4u); /* n = 5, не степень двойки */
|
||||||
|
if (v > 4) out_of_range++;
|
||||||
|
if (v == 4) seen_hi = 1;
|
||||||
|
}
|
||||||
|
TC_EQ(out_of_range, 0);
|
||||||
|
TC_TRUE(seen_hi); /* верхняя граница достижима */
|
||||||
|
|
||||||
|
pop_prandom_set(s, 7u);
|
||||||
|
out_of_range = 0;
|
||||||
|
for (i = 0; i < 64; i++) {
|
||||||
|
v = pop_prandom(&s, 1u); /* n = 2, маска */
|
||||||
|
if (v > 1) out_of_range++;
|
||||||
|
}
|
||||||
|
TC_EQ(out_of_range, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
TC_RUN(geom_xbump_layout);
|
||||||
|
TC_RUN(geom_yland_rows);
|
||||||
|
TC_RUN(geom_y_to_row);
|
||||||
|
#if POP_PRANDOM_EXACT
|
||||||
|
TC_RUN(geom_lcg_matches_reference);
|
||||||
|
TC_RUN(geom_lcg_seed_zero);
|
||||||
|
#endif
|
||||||
|
TC_RUN(geom_prandom_range);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* t_grab.c — прыжок с места через провал в три тайла с зацепом за дальнюю
|
||||||
|
* кромку (уровень 2, комната 9: Кид на краю (1,1) прыгает и должен
|
||||||
|
* повиснуть на (1,5)).
|
||||||
|
*
|
||||||
|
* Зачем отдельный набор. Трюк требует точной фазы и вручную выходит далеко
|
||||||
|
* не всегда — в SDLPoP тоже. Поэтому проверяем не «получилось с первого
|
||||||
|
* раза», а то, что действительно важно: **окно существует и достижимо**.
|
||||||
|
* Разница между «сложно» и «невозможно» — это разница между «так в
|
||||||
|
* оригинале» и «баг у нас», и глазами в MAME её не различить.
|
||||||
|
*
|
||||||
|
* Два утверждения:
|
||||||
|
* 1. есть хотя бы одна стартовая X, из которой зацеп удаётся;
|
||||||
|
* 2. короткий шаг (Shift+вперёд) приводит Кида В ЭТО окно — иначе окно
|
||||||
|
* было бы недостижимо для игрока, что равно его отсутствию.
|
||||||
|
*
|
||||||
|
* При провале в отчёт уходит карта исходов по всей ширине плиты:
|
||||||
|
* G — зацепился, L — приземлился на дальнюю плиту, . — сорвался.
|
||||||
|
*
|
||||||
|
* Механику зацепа задаёт check_grab (seg006:0A28): она работает ТОЛЬКО на
|
||||||
|
* кадрах 102..105 (начало падения) и только при зажатом Shift. mininim,
|
||||||
|
* для сравнения, разрешает повиснуть прямо в дуге прыжка (kid-jump.c,
|
||||||
|
* кадры 7..10) — но это переработка, а не порт, и источник истины у нас
|
||||||
|
* SDLPoP.
|
||||||
|
*/
|
||||||
|
#include "tcheck.h"
|
||||||
|
#include "scene.h"
|
||||||
|
#include "stubs.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
|
||||||
|
#define E 0
|
||||||
|
#define F 1
|
||||||
|
#define P 3
|
||||||
|
#define W 20
|
||||||
|
|
||||||
|
/* Уровень 2, комната 9, ряд 1: факел(0), кнопка(1), провал(2,3,4),
|
||||||
|
* колонна(5). Ряд 2 под провалом пуст — падение уходит из комнаты. */
|
||||||
|
static const uint8_t room9[30] = {
|
||||||
|
E, E, E, E, E, W, W, W, W, W,
|
||||||
|
F, F, E, E, E, P, F, F, F, P,
|
||||||
|
W, W, E, E, E, P, F, W, W, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Судить по curr_row нельзя: у улетающего вниз Кида ряд заворачивается
|
||||||
|
* (0,1,2,0,…), и падение выглядит как «остался наверху». Надёжный признак
|
||||||
|
* — ДЕЙСТВИЕ: 2 = вис/подтягивание, 6 = вис у стены. */
|
||||||
|
#define ACT_STAND 0
|
||||||
|
#define ACT_RUN_JUMP 1
|
||||||
|
#define ACT_HANG_CLIMB 2
|
||||||
|
#define ACT_HANG_STRAIGHT 6
|
||||||
|
|
||||||
|
#define X_LO 76
|
||||||
|
#define X_HI 99
|
||||||
|
|
||||||
|
static char probe(uint8_t x, uint8_t k1, uint8_t k2)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
sc_room(room9, 9);
|
||||||
|
sc_kid_at_x(1, 1, x, 0 /* лицом вправо */);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(k1, 4); /* фаза 1: ↑+вперёд — прыжок с места */
|
||||||
|
sc_run(k2, 26); /* фаза 2: Shift — попытка зацепа */
|
||||||
|
|
||||||
|
for (i = 0; i < sc_len; i++) {
|
||||||
|
uint8_t a = sc_trace[i].action;
|
||||||
|
if (a == ACT_HANG_CLIMB || a == ACT_HANG_STRAIGHT) return 'G';
|
||||||
|
}
|
||||||
|
i = sc_len ? (uint8_t)(sc_len - 1) : 0;
|
||||||
|
if (sc_trace[i].col >= 5 &&
|
||||||
|
(sc_trace[i].action == ACT_STAND || sc_trace[i].action == ACT_RUN_JUMP))
|
||||||
|
return 'L';
|
||||||
|
return '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put(char c)
|
||||||
|
{
|
||||||
|
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void puts_(const char *s) { while (*s) put(*s++); }
|
||||||
|
|
||||||
|
/* Карта исходов — только когда что-то сломалось: по ней сразу видно,
|
||||||
|
* окно съехало или исчезло. */
|
||||||
|
static void dump_map(const char *tag, uint8_t k1, uint8_t k2)
|
||||||
|
{
|
||||||
|
uint8_t x;
|
||||||
|
puts_(tag);
|
||||||
|
for (x = X_LO; x <= X_HI; x++) put(probe(x, k1, k2));
|
||||||
|
put('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Куда встаёт Кид после n-го короткого шага от x0. */
|
||||||
|
static uint8_t step_x(uint8_t x0, uint8_t nsteps)
|
||||||
|
{
|
||||||
|
uint8_t n, fx = x0;
|
||||||
|
sc_room(room9, 9);
|
||||||
|
sc_kid_at_x(1, 1, x0, 0);
|
||||||
|
for (n = 0; n < nsteps; n++) {
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_SHIFT | SC_R, 4); /* шаг = нажатие, не удержание */
|
||||||
|
sc_run(0, 6); /* отпустить, дать доиграть */
|
||||||
|
if (sc_len) fx = sc_trace[sc_len - 1].x;
|
||||||
|
}
|
||||||
|
return fx;
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(grab_window_exists)
|
||||||
|
{
|
||||||
|
uint8_t x, n = 0, lo = 0, hi = 0;
|
||||||
|
for (x = X_LO; x <= X_HI; x++)
|
||||||
|
if (probe(x, SC_U | SC_R, SC_SHIFT) == 'G') {
|
||||||
|
if (!n) lo = x;
|
||||||
|
hi = x;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
/* Окно обязано существовать: без него трюк непроходим в принципе, а
|
||||||
|
* уровень 2 без него не пройти. */
|
||||||
|
TC_TRUE(n > 0);
|
||||||
|
if (!n) {
|
||||||
|
dump_map("no-window: ", SC_U | SC_R, SC_SHIFT);
|
||||||
|
} else {
|
||||||
|
/* Границы фиксируем: если поедут — это осознанное изменение физики,
|
||||||
|
* а не случайность. */
|
||||||
|
TC_EQ(lo, 91);
|
||||||
|
TC_EQ(hi, 95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(grab_window_reachable_by_safe_step)
|
||||||
|
{
|
||||||
|
/* Игрок доходит до кромки короткими шагами. Если они не попадают в
|
||||||
|
* окно, существование окна ничего не стоит. */
|
||||||
|
TC_EQ(step_x(80, 1), 91);
|
||||||
|
TC_EQ(step_x(80, 2), 95);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(grab_same_for_all_key_patterns)
|
||||||
|
{
|
||||||
|
/* Живой игрок держит «вперёд» и жмёт Shift по-разному. Исход не должен
|
||||||
|
* зависеть от того, отпущена ли стрелка и когда нажат Shift. */
|
||||||
|
uint8_t x;
|
||||||
|
for (x = X_LO; x <= X_HI; x++) {
|
||||||
|
char a = probe(x, SC_U | SC_R, SC_SHIFT);
|
||||||
|
char b = probe(x, SC_U | SC_R, SC_SHIFT | SC_R);
|
||||||
|
char c = probe(x, SC_U | SC_R | SC_SHIFT, SC_SHIFT | SC_R);
|
||||||
|
TC_EQ(b, a);
|
||||||
|
TC_EQ(c, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
sc_init();
|
||||||
|
TC_RUN(grab_window_exists);
|
||||||
|
TC_RUN(grab_window_reachable_by_safe_step);
|
||||||
|
TC_RUN(grab_same_for_all_key_patterns);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* t_phys.c — характеризация физики Кида (pop_map + pop_kid + pop_ctrl).
|
||||||
|
*
|
||||||
|
* Назначение набора — СТРАХОВКА ПОД РЕФАКТОРИНГ, а не проверка
|
||||||
|
* правильности. Эталоны здесь сняты с текущего билда, то есть
|
||||||
|
* консервируют и текущие баги; правильность по-прежнему проверяется
|
||||||
|
* против SDLPoP. Смысл в другом: перенос физики с `Kid.` на `Char.`
|
||||||
|
* обязан быть поведение-сохраняющим, и «трасса до == трасса после» ловит
|
||||||
|
* ровно тот класс ошибок, который там возможен.
|
||||||
|
*
|
||||||
|
* Поэтому при осознанном изменении поведения эталон ПЕРЕСНИМАЕТСЯ (см.
|
||||||
|
* SC_RECORD ниже), а в коммите должно быть написано, почему трасса
|
||||||
|
* поменялась. Молча обновлённый эталон обесценивает весь набор.
|
||||||
|
*
|
||||||
|
* Снятие эталона:
|
||||||
|
* make -C applications/PoP/roomtest/tests-host t_phys SC_RECORD=1
|
||||||
|
* Трассы уходят в отчёт tcheck готовыми C-строками — вставить в golden_*.
|
||||||
|
*/
|
||||||
|
#include "tcheck.h"
|
||||||
|
#include "scene.h"
|
||||||
|
#include "stubs.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
|
||||||
|
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
|
||||||
|
#define E 0
|
||||||
|
#define F 1
|
||||||
|
#define P 3
|
||||||
|
#define L 11
|
||||||
|
#define W 20
|
||||||
|
|
||||||
|
/* Ровный пол во всю комнату: базовая сцена для ходьбы/бега/разворота. */
|
||||||
|
static const uint8_t room_flat[30] = {
|
||||||
|
W, E, E, E, E, E, E, E, E, E,
|
||||||
|
W, F, F, F, F, F, F, F, F, F,
|
||||||
|
W, W, W, W, W, W, W, W, W, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Пол с обрывом после колонки 5: падение с ряда 1 на ряд 2. */
|
||||||
|
static const uint8_t room_ledge[30] = {
|
||||||
|
W, E, E, E, E, E, E, E, E, E,
|
||||||
|
W, F, F, F, F, F, E, E, E, E,
|
||||||
|
W, F, F, F, F, F, F, F, F, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Проваливающаяся плита на (1,4) — сцена под loose-механику. */
|
||||||
|
static const uint8_t room_loose[30] = {
|
||||||
|
W, E, E, E, E, E, E, E, E, E,
|
||||||
|
W, F, F, F, L, F, F, F, F, F,
|
||||||
|
W, F, F, F, F, F, F, F, F, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Провал в ТРИ пустых тайла (колонки 2,3,4) — геометрия из BUG-RJUMP-1
|
||||||
|
* (ур. 2, комнаты 1 и 9). Разбег-прыжок влево с колонки 5 обязан
|
||||||
|
* доставать до колонки 1; у нас Кид сваливается в провал. */
|
||||||
|
static const uint8_t room_gap3[30] = {
|
||||||
|
W, E, E, E, E, E, E, E, E, E,
|
||||||
|
W, F, E, E, E, F, F, F, F, F,
|
||||||
|
W, F, F, F, F, F, F, F, F, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* SC_RECORD=<n> — снять эталон СЦЕНАРИЯ n (нумерация в порядке вызовов в
|
||||||
|
* main). По одному за прогон: лог tcheck 768 байт, на все трассы сразу
|
||||||
|
* его не хватает. Перебором занимается record_golden.py. */
|
||||||
|
#ifndef SC_RECORD
|
||||||
|
#define SC_RECORD 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Снять или сверить — одной строкой в каждом сценарии, чтобы режимы не
|
||||||
|
* разъезжались. Имя сценария = имя файла эталона. */
|
||||||
|
#if SC_RECORD
|
||||||
|
static uint8_t sc_idx;
|
||||||
|
#define SC_CHECK(g, nm) \
|
||||||
|
do { if (++sc_idx == (SC_RECORD)) sc_dump(nm); } while (0)
|
||||||
|
#else
|
||||||
|
#define SC_CHECK(g, nm) sc_expect((g), (uint8_t)(sizeof(g) / SC_REC_BYTES))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ---- эталоны (сняты SC_RECORD=1) ------------------------------------ *
|
||||||
|
* В режиме снятия эталонов файлов golden/ ещё нет — и не должно быть:
|
||||||
|
* первый прогон их и порождает. */
|
||||||
|
#if !SC_RECORD
|
||||||
|
|
||||||
|
static const uint8_t golden_stand[] = {
|
||||||
|
#include "golden/stand.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_run_right[] = {
|
||||||
|
#include "golden/run_right.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_step_shift[] = {
|
||||||
|
#include "golden/step_shift.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_turn[] = {
|
||||||
|
#include "golden/turn.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_crouch[] = {
|
||||||
|
#include "golden/crouch.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_jump_up[] = {
|
||||||
|
#include "golden/jump_up.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_fall_ledge[] = {
|
||||||
|
#include "golden/fall_ledge.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_loose[] = {
|
||||||
|
#include "golden/loose.h"
|
||||||
|
};
|
||||||
|
static const uint8_t golden_rjump_gap3[] = {
|
||||||
|
#include "golden/rjump_gap3.h"
|
||||||
|
};
|
||||||
|
|
||||||
|
#else
|
||||||
|
/* SC_RECORD: имена нужны только чтобы SC_CHECK() раскрылся. */
|
||||||
|
#define golden_stand 0
|
||||||
|
#define golden_run_right 0
|
||||||
|
#define golden_step_shift 0
|
||||||
|
#define golden_turn 0
|
||||||
|
#define golden_crouch 0
|
||||||
|
#define golden_jump_up 0
|
||||||
|
#define golden_fall_ledge 0
|
||||||
|
#define golden_loose 0
|
||||||
|
#define golden_rjump_gap3 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ---- сценарии -------------------------------------------------------- */
|
||||||
|
|
||||||
|
TC_TEST(phys_stand_is_stable)
|
||||||
|
{
|
||||||
|
/* Никакого ввода: Кид на ровном полу обязан стоять неподвижно. Тест
|
||||||
|
* дешёвый, но ловит самое обидное — «сам поехал» после правки
|
||||||
|
* геометрии или окна Char. */
|
||||||
|
sc_room(room_flat, 1);
|
||||||
|
sc_kid_at(3, 1, -1);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(0, 12);
|
||||||
|
SC_CHECK(golden_stand, "stand");
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_run_right)
|
||||||
|
{
|
||||||
|
sc_room(room_flat, 1);
|
||||||
|
sc_kid_at(1, 1, 0);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_R, 24);
|
||||||
|
SC_CHECK(golden_run_right, "run_right");
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_step_with_shift)
|
||||||
|
{
|
||||||
|
/* Shift + вперёд — осторожный шаг: одна из самых чувствительных к
|
||||||
|
* distance_to_edge веток. */
|
||||||
|
sc_room(room_flat, 1);
|
||||||
|
sc_kid_at(3, 1, 0);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_R | SC_SHIFT, 20);
|
||||||
|
SC_CHECK(golden_step_shift, "step_shift");
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_turn_around)
|
||||||
|
{
|
||||||
|
sc_room(room_flat, 1);
|
||||||
|
sc_kid_at(4, 1, 0);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_L, 16);
|
||||||
|
SC_CHECK(golden_turn, "turn");
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_crouch_and_stand_up)
|
||||||
|
{
|
||||||
|
sc_room(room_flat, 1);
|
||||||
|
sc_kid_at(4, 1, 0);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_D, 8);
|
||||||
|
sc_run(0, 12);
|
||||||
|
SC_CHECK(golden_crouch, "crouch");
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_jump_up_nothing_above)
|
||||||
|
{
|
||||||
|
/* Над головой пусто — seq_28; проверяем и смещение по x, которое
|
||||||
|
* оригинал делает перед прыжком. */
|
||||||
|
sc_room(room_flat, 1);
|
||||||
|
sc_kid_at(4, 1, -1);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_U, 20);
|
||||||
|
SC_CHECK(golden_jump_up, "jump_up");
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_run_off_ledge)
|
||||||
|
{
|
||||||
|
/* Бег в обрыв: check_on_floor -> start_fall -> land на ряду 2.
|
||||||
|
* Сценарий, вокруг которого крутился BUG-FALL-SWORD-1. */
|
||||||
|
sc_room(room_ledge, 1);
|
||||||
|
sc_kid_at(1, 1, 0);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_R, 34);
|
||||||
|
SC_CHECK(golden_fall_ledge, "fall_ledge");
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_loose_floor_breaks)
|
||||||
|
{
|
||||||
|
/* Пробежка по loose-плите: тайл обязан затрястись и провалиться, а
|
||||||
|
* физика — сообщить об этом соседям (trob + перерисовка). */
|
||||||
|
sc_room(room_loose, 1);
|
||||||
|
sc_kid_at(1, 1, 0);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_R, 34);
|
||||||
|
SC_CHECK(golden_loose, "loose");
|
||||||
|
#if !SC_RECORD
|
||||||
|
/* Журнал заглушек: плита обязана была отвалиться (кусок отрывается —
|
||||||
|
* pop_loose_mob_spawn) и попросить перерисовку. Это уже не
|
||||||
|
* характеризация, а содержательная проверка — заглушка-пустышка её не
|
||||||
|
* дала бы. Через trob loose у нас НЕ идёт (только пики/ворота), у него
|
||||||
|
* свой pop_loose_modif — поэтому спрашиваем именно про кусок. */
|
||||||
|
TC_TRUE(tk_log_count(TK_MOB_SPAWN) > 0);
|
||||||
|
TC_TRUE(tk_log_count(TK_REDRAW) > 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(phys_running_jump_over_3tile_gap)
|
||||||
|
{
|
||||||
|
/* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок.
|
||||||
|
* Ожидание оригинала: перелёт провала в три тайла и посадка на (1,1).
|
||||||
|
* Сейчас эталон фиксирует НАШЕ поведение (падение в провал) — когда
|
||||||
|
* баг починим, трасса поменяется осознанно и эталон переснимется. */
|
||||||
|
sc_room(room_gap3, 1);
|
||||||
|
sc_kid_at(8, 1, -1);
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_L, 8); /* разбег */
|
||||||
|
sc_run(SC_L | SC_U, 26); /* прыжок и посадка */
|
||||||
|
SC_CHECK(golden_rjump_gap3, "rjump_gap3");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- точка входа ----------------------------------------------------- */
|
||||||
|
|
||||||
|
void main(void)
|
||||||
|
{
|
||||||
|
sc_init();
|
||||||
|
TC_RUN(phys_stand_is_stable);
|
||||||
|
TC_RUN(phys_run_right);
|
||||||
|
TC_RUN(phys_step_with_shift);
|
||||||
|
TC_RUN(phys_turn_around);
|
||||||
|
TC_RUN(phys_crouch_and_stand_up);
|
||||||
|
TC_RUN(phys_jump_up_nothing_above);
|
||||||
|
TC_RUN(phys_run_off_ledge);
|
||||||
|
TC_RUN(phys_loose_floor_breaks);
|
||||||
|
TC_RUN(phys_running_jump_over_3tile_gap);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* t_wall.c — удар о стену В ВОЗДУХЕ (BUG-JUMPWALL-1).
|
||||||
|
*
|
||||||
|
* Наблюдение с приёмки уровня 3 (комната 14): Кид прыгает, не долетает и
|
||||||
|
* вместо удара о кладку с последующим сползанием ВДОЛЬ неё пролетает
|
||||||
|
* сквозь стену «по свободной траектории». Класс отказа шире одной
|
||||||
|
* комнаты, поэтому проверяем его отдельным набором, а не сценарием в
|
||||||
|
* t_phys: там характеризация (трасса == эталон), здесь — утверждение о
|
||||||
|
* поведении («сквозь кладку нельзя»), которое не должно молча
|
||||||
|
* переснашиваться вместе с эталонами.
|
||||||
|
*
|
||||||
|
* Оригинал: bumped_fall (seg004:04E4) на ударе гасит `Char.fall_x`, то есть
|
||||||
|
* горизонтальный снос обнуляется и дальше Кид падает вертикально. Кто
|
||||||
|
* решает, что удар был, — check_collisions (seg004:0004) + check_bumped.
|
||||||
|
*
|
||||||
|
* Проверка — свипом по стартовому X, а не одним прыжком: исход зависит от
|
||||||
|
* фазы, и «с одной X работает» ничего не доказывает (ровно так и вышло с
|
||||||
|
* первым сценарием — из центра плиты стена держит).
|
||||||
|
*/
|
||||||
|
#include "tcheck.h"
|
||||||
|
#include "scene.h"
|
||||||
|
#include "stubs.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
|
||||||
|
#define E 0
|
||||||
|
#define F 1
|
||||||
|
#define W 20
|
||||||
|
|
||||||
|
/* Комната 14 уровня 3 с одной правкой: шахте колонок 6–7 подложено дно
|
||||||
|
* (ряд 2), потому что соседних комнат у сцены нет, а в игре Кид улетает
|
||||||
|
* вниз в комнату 13. Остальное — как в данных: площадка (0,8), провал
|
||||||
|
* (0,6)/(0,7), СТЕНА в колонке 5 рядов 1–2, площадка (0,5) за ней. */
|
||||||
|
static const uint8_t room14[30] = {
|
||||||
|
F, F, F, E, E, F, E, E, F, E,
|
||||||
|
W, W, W, E, E, W, E, E, W, E,
|
||||||
|
W, W, W, F, F, W, F, F, W, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Плита (0,8) по X: колонка 8 — это [177, 191), плюс шов с пустой (0,9). */
|
||||||
|
#define X_LO 177
|
||||||
|
#define X_HI 196
|
||||||
|
|
||||||
|
/* Прыгнуть влево со стартовой X. Возврат — код исхода:
|
||||||
|
* 'L' — долетел до площадки (0,5) и стоит на ней (ряд 0) — законно;
|
||||||
|
* '.' — не долетел и упал на дно шахты колонок 6–7 — тоже законно;
|
||||||
|
* цифра — колонка, в которой Кид оказался ЛЕВЕЕ стены, то есть прошёл
|
||||||
|
* сквозь кладку колонки 5 (это и есть баг). */
|
||||||
|
static char jump_left_from(uint8_t x)
|
||||||
|
{
|
||||||
|
sc_room(room14, 14);
|
||||||
|
sc_kid_at_x(8, 0, x, -1 /* лицом влево */);
|
||||||
|
sc_trace_clear();
|
||||||
|
/* Клавиши держим только на сам прыжок: если удерживать их всю сцену,
|
||||||
|
* Кид, ДОЛЕТЕВШИЙ до (0,5), тут же прыгает с неё второй раз — и исход
|
||||||
|
* говорит уже не о стене, а о втором прыжке. */
|
||||||
|
sc_run(SC_L | SC_U, 6);
|
||||||
|
sc_run(0, 34);
|
||||||
|
if (!sc_len) return '?';
|
||||||
|
{
|
||||||
|
int8_t col = sc_trace[sc_len - 1].col;
|
||||||
|
int8_t row = sc_trace[sc_len - 1].row;
|
||||||
|
if (row == 0) return 'L'; /* остался на верхнем ряду */
|
||||||
|
if (col >= 6) return '.'; /* сполз вдоль стены в шахту */
|
||||||
|
return (char)('0' + (col < 0 ? 0 : col));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put(char c)
|
||||||
|
{
|
||||||
|
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
|
||||||
|
}
|
||||||
|
static void puts_(const char *s) { while (*s) put(*s++); }
|
||||||
|
|
||||||
|
TC_TEST(wall_stops_undershot_jump)
|
||||||
|
{
|
||||||
|
uint8_t x, bad = 0;
|
||||||
|
/* Карта исходов по всей ширине плиты: 'L' — долетел до (0,5), '.' —
|
||||||
|
* сполз вдоль стены в шахту, цифра — просочился сквозь кладку. */
|
||||||
|
puts_("x=177..196: ");
|
||||||
|
for (x = X_LO; x <= X_HI; x++) {
|
||||||
|
char r = jump_left_from(x);
|
||||||
|
put(r);
|
||||||
|
if (r != 'L' && r != '.') bad++;
|
||||||
|
}
|
||||||
|
put('\n');
|
||||||
|
TC_EQ(bad, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
sc_init();
|
||||||
|
TC_RUN(wall_stops_undershot_jump);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -14,7 +14,9 @@ seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src"
|
SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "SDLPoP", "src")
|
||||||
|
OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"..", "poc", "res", "kid")
|
||||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
"..", "roomtest", "kid_data.h")
|
"..", "roomtest", "kid_data.h")
|
||||||
SEQTBL_BASE = 0x196E
|
SEQTBL_BASE = 0x196E
|
||||||
@@ -24,10 +26,11 @@ def read(name):
|
|||||||
return open(os.path.join(SRC, name)).read()
|
return open(os.path.join(SRC, name)).read()
|
||||||
|
|
||||||
|
|
||||||
def parse_frame_table(txt):
|
def parse_frame_table(txt, name="frame_table_kid"):
|
||||||
"""frame_table_kid[] -> список (image, dx, dy, flags). Записи вида
|
"""frame_table_kid[]/frame_tbl_guard[] -> список (image, dx, dy, flags).
|
||||||
{ IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как OR-выражения."""
|
Записи вида { IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как
|
||||||
m = re.search(r'const frame_type frame_table_kid\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
|
OR-выражения."""
|
||||||
|
m = re.search(r'const frame_type ' + name + r'\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
|
||||||
body = m.group(1)
|
body = m.group(1)
|
||||||
frames = []
|
frames = []
|
||||||
for row in re.findall(r'\{([^}]*)\}', body):
|
for row in re.findall(r'\{([^}]*)\}', body):
|
||||||
@@ -41,6 +44,18 @@ def parse_frame_table(txt):
|
|||||||
return frames
|
return frames
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sword_tbl(txt):
|
||||||
|
"""sword_tbl[] (seg006:2047) -> список (id, x, y). id 255 = клинка нет.
|
||||||
|
Индекс — sword_frame (cur_frame.sword & 0x3F); один и тот же chtab_0 и у
|
||||||
|
Кида, и у стража."""
|
||||||
|
m = re.search(r'const sword_table_type sword_tbl\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
|
||||||
|
rows = []
|
||||||
|
for row in re.findall(r'\{([^}]*)\}', m.group(1)):
|
||||||
|
a, b, c = [int(p.strip(), 0) for p in row.split(',')]
|
||||||
|
rows.append((a & 0xFF, b & 0xFF, c & 0xFF))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def parse_byte_array(txt, name):
|
def parse_byte_array(txt, name):
|
||||||
m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
|
m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
|
||||||
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
|
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
|
||||||
@@ -63,8 +78,10 @@ def main():
|
|||||||
seqc = read("seqtbl.c")
|
seqc = read("seqtbl.c")
|
||||||
|
|
||||||
frames = parse_frame_table(seg006)
|
frames = parse_frame_table(seg006)
|
||||||
|
gframes = parse_frame_table(seg006, "frame_tbl_guard")
|
||||||
seqtbl = parse_byte_array(seqc, "original_seqtbl")
|
seqtbl = parse_byte_array(seqc, "original_seqtbl")
|
||||||
offs = parse_word_array(seqc, "original_seqtbl_offsets")
|
offs = parse_word_array(seqc, "original_seqtbl_offsets")
|
||||||
|
swtbl = parse_sword_tbl(seg006)
|
||||||
|
|
||||||
with open(OUT, "w") as f:
|
with open(OUT, "w") as f:
|
||||||
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
|
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
|
||||||
@@ -74,28 +91,57 @@ def main():
|
|||||||
f.write("#ifndef KID_DATA_H\n#define KID_DATA_H\n#include <stdint.h>\n\n")
|
f.write("#ifndef KID_DATA_H\n#define KID_DATA_H\n#include <stdint.h>\n\n")
|
||||||
f.write("#define SEQTBL_BASE 0x%04Xu\n\n" % SEQTBL_BASE)
|
f.write("#define SEQTBL_BASE 0x%04Xu\n\n" % SEQTBL_BASE)
|
||||||
|
|
||||||
# frame table
|
# frame table и seqtbl В .h НЕ ПОПАДАЮТ — они уезжают в kid_data.bin
|
||||||
f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */\n")
|
# (EMM-страница): 3.5 КБ в _CODE окна W1/W2 нам дороже, чем один
|
||||||
|
# gfx_w0_map за тик. См. applications/PoP/docs/layout_plan_v2.md §5.1.
|
||||||
|
f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid).\n"
|
||||||
|
" * САМА таблица кадров и seqtbl лежат в kid_data.bin (EMM-\n"
|
||||||
|
" * страница); здесь только тип и размеры. */\n")
|
||||||
f.write("typedef struct { uint8_t image; int8_t dx, dy; "
|
f.write("typedef struct { uint8_t image; int8_t dx, dy; "
|
||||||
"uint8_t flags, sword; } kframe;\n")
|
"uint8_t flags, sword; } kframe;\n")
|
||||||
f.write("#define KID_NFRAMES %d\n" % len(frames))
|
f.write("#define KID_NFRAMES %d\n" % len(frames))
|
||||||
f.write("static const kframe kid_frames[KID_NFRAMES] = {\n")
|
f.write("#define KID_SEQTBL_LEN %d\n\n" % len(seqtbl))
|
||||||
for (img, dx, dy, fl, sw) in frames:
|
|
||||||
dxs = dx - 256 if dx > 127 else dx
|
|
||||||
dys = dy - 256 if dy > 127 else dy
|
|
||||||
f.write(" {%3d,%4d,%4d,0x%02X,0x%02X},\n" % (img, dxs, dys, fl, sw))
|
|
||||||
f.write("};\n\n")
|
|
||||||
|
|
||||||
# seqtbl bytes
|
# раскладка kid_data.bin (смещения ОТ НАЧАЛА ДАННЫХ страницы)
|
||||||
f.write("#define KID_SEQTBL_LEN %d\n" % len(seqtbl))
|
f.write("/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */\n")
|
||||||
f.write("static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {\n")
|
f.write("#define KID_BIN_FRAMES_OFF 0\n")
|
||||||
f.write(emit_c(seqtbl) + "\n};\n\n")
|
f.write("#define KID_BIN_SEQTBL_OFF %d\n" % (len(frames) * 5))
|
||||||
|
f.write("/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс\n"
|
||||||
|
" * frame + add_frame - 149 (add_frame=70 для кадров 102..106). */\n")
|
||||||
|
f.write("#define KID_NGFRAMES %d\n" % len(gframes))
|
||||||
|
f.write("#define KID_BIN_GFRAMES_OFF %d\n\n" % (len(frames) * 5 + len(seqtbl)))
|
||||||
|
|
||||||
# seq offsets
|
# seq offsets
|
||||||
f.write("#define KID_NSEQ %d\n" % len(offs))
|
f.write("#define KID_NSEQ %d\n" % len(offs))
|
||||||
f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n")
|
f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n")
|
||||||
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
|
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
|
||||||
f.write("#endif\n")
|
|
||||||
|
# sword_tbl — макро-инициализаторами, а не готовыми массивами: так
|
||||||
|
# 159 байт таблицы лягут в ОДИН TU (pop_kid.c), а не в каждый, кто
|
||||||
|
# включил заголовок.
|
||||||
|
f.write("/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.\n"
|
||||||
|
" * Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.\n"
|
||||||
|
" * Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),\n"
|
||||||
|
" * x — «вперёд» по направлению персонажа. */\n")
|
||||||
|
f.write("#define KID_NSWORD %d\n" % len(swtbl))
|
||||||
|
for i, (nm, fmt) in enumerate((("ID", "0x%02X"), ("DX", "%d"), ("DY", "%d"))):
|
||||||
|
# dx/dy — знаковые: печатаем со знаком, иначе int8_t = overflow
|
||||||
|
vals = [r[i] if i == 0 else (r[i] - 256 if r[i] > 127 else r[i])
|
||||||
|
for r in swtbl]
|
||||||
|
f.write("#define KID_SWORD_%s_INIT { \\\n" % nm)
|
||||||
|
f.write(emit_c(vals, 12, fmt).replace("\n", " \\\n") + " \\\n}\n")
|
||||||
|
f.write("\n#endif\n")
|
||||||
|
|
||||||
|
# kid_data.bin: кадры (по 5 байт) + seqtbl подряд
|
||||||
|
os.makedirs(OUT_BIN_DIR, exist_ok=True)
|
||||||
|
binpath = os.path.join(OUT_BIN_DIR, "kid_data.bin")
|
||||||
|
with open(binpath, "wb") as b:
|
||||||
|
for (img, dx, dy, fl, sw) in frames:
|
||||||
|
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
|
||||||
|
b.write(bytes(seqtbl))
|
||||||
|
for (img, dx, dy, fl, sw) in gframes:
|
||||||
|
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
|
||||||
|
print(f"kid_data.bin: {len(frames)*5 + len(seqtbl) + len(gframes)*5} Б (кадры Kid + seqtbl + кадры стража) -> {binpath}")
|
||||||
|
|
||||||
print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, "
|
print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, "
|
||||||
f"{len(offs)} seq-оффсетов -> {OUT}")
|
f"{len(offs)} seq-оффсетов -> {OUT}")
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
pop_pack_guard.py — упаковщик спрайтов СТРАЖА (chtab_5_guard, база 750).
|
||||||
|
|
||||||
|
Оригинал грузит стражей из отдельного .DAT по типу уровня
|
||||||
|
(GUARD/FAT/SKEL/VIZIER/SHADOW, seg000:1092); уровень 1 — обычный «guard»,
|
||||||
|
то есть data/GUARD/res(750+id).png. Палитра — НЕ из PNG: оригинал
|
||||||
|
подменяет её из res10.bin (guard_palettes, 7 палитр по 16 цветов, 6-бит) по
|
||||||
|
level.guards_color; на уровне 1 у обоих стражей color = 2.
|
||||||
|
|
||||||
|
Выход: poc/res/guard/g0..gN.atl (по 8 спрайтов на страницу, адресация
|
||||||
|
id>>3 / id&7 — как у Kid) + группа палитры 0x90..0x9F в общий kid.pal
|
||||||
|
(его собирает pop_pack_kid.py, см. GUARD_PAL_BASE там).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, HERE)
|
||||||
|
import pop_pack_bg as B
|
||||||
|
from pop_pack_kid import transpose_cols
|
||||||
|
|
||||||
|
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD")
|
||||||
|
PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin")
|
||||||
|
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard")
|
||||||
|
PAL_BASE = 0x90 # слоты палитры Sprinter под стража
|
||||||
|
COLOR = 2 # level.guards_color обоих стражей уровня 1
|
||||||
|
SHIFT = 3 # g[id>>3], idx id&7 (спрайты крупные)
|
||||||
|
MASK = (1 << SHIFT) - 1
|
||||||
|
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
|
||||||
|
|
||||||
|
|
||||||
|
def load_guard(id):
|
||||||
|
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
|
||||||
|
COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
|
||||||
|
|
||||||
|
Off-by-one как у Kid (см. pop_pack_kid.load_kid): load_chtab_from_file
|
||||||
|
зовётся с ресурсом 750, а images[i-1] = res(750+i), то есть images[0] =
|
||||||
|
res751. Рисование берёт get_image(chtab, obj_id+1 − 1) = images[image],
|
||||||
|
значит frame.image=N -> res(751+N). С res(750+N) страж стоял в позе
|
||||||
|
предыдущего кадра (замечено на frame_166_stand_inactive: image 17
|
||||||
|
рисовался как res767 — выпад — вместо res768). """
|
||||||
|
path = os.path.join(GUARD_DIR, f"res{751 + id}.png")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return None
|
||||||
|
im = Image.open(path)
|
||||||
|
if im.mode != "P":
|
||||||
|
B.die(f"res{750+id}: mode {im.mode}, ожидался P")
|
||||||
|
src = im.tobytes()
|
||||||
|
rowmajor = bytes((0xFF if v == 0 else PAL_BASE + v) for v in src)
|
||||||
|
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
|
||||||
|
|
||||||
|
|
||||||
|
def guard_palette():
|
||||||
|
"""16 цветов (r,g,b 0..63) из res10.bin для COLOR."""
|
||||||
|
b = open(PAL_BIN, "rb").read()
|
||||||
|
off = 0x30 * COLOR - 0x30
|
||||||
|
return [(b[off + i * 3], b[off + i * 3 + 1], b[off + i * 3 + 2]) for i in range(16)]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
pages = {}
|
||||||
|
n = 0
|
||||||
|
for id in range(0, MAX_ID + 1):
|
||||||
|
g = load_guard(id)
|
||||||
|
if g is None:
|
||||||
|
continue
|
||||||
|
pages.setdefault(id >> SHIFT, {})[id & MASK] = g
|
||||||
|
n += 1
|
||||||
|
if not pages:
|
||||||
|
B.die("нет спрайтов стража")
|
||||||
|
total = 0
|
||||||
|
for pg in range(max(pages) + 1):
|
||||||
|
nm = f"g{pg}.atl"
|
||||||
|
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
|
||||||
|
total += sz
|
||||||
|
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б")
|
||||||
|
print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -25,7 +25,11 @@ KID_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDL
|
|||||||
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
|
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
|
||||||
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
|
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
|
||||||
SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F
|
SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F
|
||||||
SWORD_IDS = tuple(range(20, 28)) # sword_tbl для кадров 229..236 (найти-убрать)
|
GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
|
||||||
|
# chtab_0 целиком: sword_tbl (seg006:2047) адресует id 0..33 — подъём меча,
|
||||||
|
# ножны И вся боёвка. Меч в оригинале ОДИН на всех: и Кид, и страж рисуют
|
||||||
|
# клинок из этого же chtab_0 (add_sword_to_objtable, seg006:1798).
|
||||||
|
SWORD_IDS = tuple(range(0, 34))
|
||||||
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
|
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
|
||||||
MASK = (1 << SHIFT) - 1
|
MASK = (1 << SHIFT) - 1
|
||||||
|
|
||||||
@@ -108,6 +112,14 @@ def build_palette(out_path):
|
|||||||
for i, (r, g, bl) in enumerate(_read16(path)):
|
for i, (r, g, bl) in enumerate(_read16(path)):
|
||||||
o = (base + i) * 4
|
o = (base + i) * 4
|
||||||
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
||||||
|
# Палитра стража берётся НЕ из .pal, а из res10.bin (guard_palettes:
|
||||||
|
# 7 палитр по 16 цветов, 6-бит) по level.guards_color; на уровне 1 у
|
||||||
|
# обоих стражей color = 2. См. toolchain/pop_pack_guard.py.
|
||||||
|
import pop_pack_guard as G
|
||||||
|
for i, (r, g, bl) in enumerate(G.guard_palette()):
|
||||||
|
o = (GUARD_PAL_BASE + i) * 4
|
||||||
|
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
||||||
|
|
||||||
with open(out_path, "wb") as f:
|
with open(out_path, "wb") as f:
|
||||||
f.write(pal)
|
f.write(pal)
|
||||||
|
|
||||||
@@ -141,8 +153,7 @@ def main():
|
|||||||
names.append(None)
|
names.append(None)
|
||||||
print(f" (kid стр {pg}: пусто)")
|
print(f" (kid стр {pg}: пусто)")
|
||||||
|
|
||||||
# chtab_0: меч в руке. Пакуем ТОЛЬКО кадры подъёма/ножен (sword_tbl
|
# chtab_0: меч в руке — весь chtab (id 0..33), индекс в атласе = id.
|
||||||
# 35..42 -> id 20..27); индекс в атласе = id - 20 (бой не портирован).
|
|
||||||
sw = {}
|
sw = {}
|
||||||
for i, id in enumerate(SWORD_IDS):
|
for i, id in enumerate(SWORD_IDS):
|
||||||
w, h, pix = load_sword(id)
|
w, h, pix = load_sword(id)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
# CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3
|
# CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3
|
||||||
# Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3
|
# Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3
|
||||||
# -I PATH additional include path (repeatable)
|
# -I PATH additional include path (repeatable)
|
||||||
|
# -DNAME[=VAL] preprocessor define, passed to sdcc as is (repeatable)
|
||||||
# -L 0xADDR code load address (default: derived from --memory mode)
|
# -L 0xADDR code load address (default: derived from --memory mode)
|
||||||
# -E 0xADDR entry address (default: same as -L)
|
# -E 0xADDR entry address (default: same as -L)
|
||||||
# -S 0xADDR stack address (default: 0xBFFE)
|
# -S 0xADDR stack address (default: 0xBFFE)
|
||||||
@@ -32,6 +33,9 @@
|
|||||||
# --data-loc 0xN override SDCC --data-loc (default: derived from --memory)
|
# --data-loc 0xN override SDCC --data-loc (default: derived from --memory)
|
||||||
# -Wl FLAG extra linker flag (repeatable)
|
# -Wl FLAG extra linker flag (repeatable)
|
||||||
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
|
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
|
||||||
|
# --bank-data put bank modules' writable data INTO the bank page
|
||||||
|
# (default: it goes to the shared _DATA in W1/W2, which
|
||||||
|
# stays mapped and is visible from everywhere)
|
||||||
# automatically and adds -Wl-b_BANKN=0x{N}C000
|
# automatically and adds -Wl-b_BANKN=0x{N}C000
|
||||||
# --w3 FILE.c place FILE.c resident in window 3 (0xC000), called
|
# --w3 FILE.c place FILE.c resident in window 3 (0xC000), called
|
||||||
# DIRECTLY (no trampoline). Repeatable. Defaults to
|
# DIRECTLY (no trampoline). Repeatable. Defaults to
|
||||||
@@ -106,6 +110,8 @@ LD_EXTRA=()
|
|||||||
MKEXE_EXTRA=()
|
MKEXE_EXTRA=()
|
||||||
BANK_SPECS=() # entries like "1=engine.c"
|
BANK_SPECS=() # entries like "1=engine.c"
|
||||||
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
|
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
|
||||||
|
USER_DEFS=() # -DFOO / -DFOO=bar — пробрасываются в sdcc как есть
|
||||||
|
BANK_LOCAL_DATA=0 # 1 (--bank-data): писучие данные банков — В СТРАНИЦУ банка
|
||||||
W3_RELS=() # заполняется при компиляции W3-модулей
|
W3_RELS=() # заполняется при компиляции W3-модулей
|
||||||
W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3
|
W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3
|
||||||
USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small)
|
USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small)
|
||||||
@@ -139,7 +145,9 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--w3) W3_SPECS+=("$2"); shift 2;;
|
--w3) W3_SPECS+=("$2"); shift 2;;
|
||||||
--stack-size) STACK_SIZE="$2"; shift 2;;
|
--stack-size) STACK_SIZE="$2"; shift 2;;
|
||||||
-Wl) LD_EXTRA+=("$2"); shift 2;;
|
-Wl) LD_EXTRA+=("$2"); shift 2;;
|
||||||
|
-D*) USER_DEFS+=("$1"); shift;;
|
||||||
--bank) BANK_SPECS+=("$2"); shift 2;;
|
--bank) BANK_SPECS+=("$2"); shift 2;;
|
||||||
|
--bank-data) BANK_LOCAL_DATA=1; shift;;
|
||||||
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
|
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
|
||||||
--max-allocs) MAX_ALLOCS="$2"; shift 2;;
|
--max-allocs) MAX_ALLOCS="$2"; shift 2;;
|
||||||
--gfx) GFX_MODE="$2"; shift 2;;
|
--gfx) GFX_MODE="$2"; shift 2;;
|
||||||
@@ -366,7 +374,7 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
|
|||||||
|
|
||||||
# 2. user sources → .rel (HOME)
|
# 2. user sources → .rel (HOME)
|
||||||
USER_RELS=()
|
USER_RELS=()
|
||||||
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}")
|
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}" "${USER_DEFS[@]}")
|
||||||
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
|
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
|
||||||
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
|
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
|
||||||
for src in "${SOURCES[@]}"; do
|
for src in "${SOURCES[@]}"; do
|
||||||
@@ -420,8 +428,19 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then
|
|||||||
bank_n="${spec%%=*}"
|
bank_n="${spec%%=*}"
|
||||||
bank_src="${spec#*=}"
|
bank_src="${spec#*=}"
|
||||||
rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel"
|
rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel"
|
||||||
|
# По умолчанию --dataseg НЕ задаём: писучие данные банкового
|
||||||
|
# модуля (глобалы, file-static, статики функций) остаются в
|
||||||
|
# общем _DATA, то есть в W1/W2 — они замаплены всегда и видны
|
||||||
|
# и банку, и остальному коду. С --dataseg BANKn они уезжают в
|
||||||
|
# СТРАНИЦУ БАНКА и снаружи не читаются (проверено на .map:
|
||||||
|
# глобал банкового модуля лёг по 0x0001C000): это экономит
|
||||||
|
# W1/W2, но требует ручного контроля видимости, поэтому
|
||||||
|
# включается явно через --bank-data.
|
||||||
|
bank_data_flags=()
|
||||||
|
[[ $BANK_LOCAL_DATA -eq 1 ]] && bank_data_flags=(--dataseg "BANK${bank_n}")
|
||||||
run "$SDCC" "${CC_FLAGS[@]}" \
|
run "$SDCC" "${CC_FLAGS[@]}" \
|
||||||
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \
|
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" \
|
||||||
|
"${bank_data_flags[@]}" \
|
||||||
-c -o "$rel" "$bank_src"
|
-c -o "$rel" "$bank_src"
|
||||||
BANK_RELS+=("$rel")
|
BANK_RELS+=("$rel")
|
||||||
# Virtual address: bank_n in upper byte, BANK_LOW16 in low half.
|
# Virtual address: bank_n in upper byte, BANK_LOW16 in low half.
|
||||||
|
|||||||