Compare commits
47 Commits
cf06896dbd
...
main
| 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 |
@@ -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.
|
||||||
|
|
||||||
|
Правило приёмки: тест не считается написанным, пока не проверен мутацией —
|
||||||
|
сломать проверяемое место и убедиться, что набор краснеет.
|
||||||
@@ -4,6 +4,63 @@
|
|||||||
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
||||||
сделать это прямо сейчас.
|
сделать это прямо сейчас.
|
||||||
|
|
||||||
|
## Зелье «переворот экрана» (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,
|
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
# 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 по свежему замеру.
|
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||||
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
|
Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
|
||||||
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
|
как полностью перекрытый этим документом. Документ самодостаточный
|
||||||
— рассчитан на старт с пустого контекста.
|
— рассчитан на старт с пустого контекста.
|
||||||
|
|
||||||
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
||||||
@@ -442,5 +460,54 @@ dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
|
|||||||
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
||||||
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
||||||
`libc_one_function_per_module`.
|
`libc_one_function_per_module`.
|
||||||
- `applications/PoP/docs/size_optimization_plan.md` — v1 (замер 2026-07-21,
|
- `applications/PoP/roomtest/TASKS.md` — что из этого берётся в работу сейчас.
|
||||||
раздел §8 про скорость отрисовки актуален и не дублируется здесь).
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
## Модули
|
## Модули
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,44 @@ MEMORY ?= huge # small-раскладка + банки кода в W
|
|||||||
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
|
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
|
||||||
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
|
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
|
||||||
# sdcc_banked_call_rules.
|
# sdcc_banked_call_rules.
|
||||||
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 2=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c $(PROF_FLAGS)
|
# Банк 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) — ВКЛЮЧЕНО по
|
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
||||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||||
# MAME = время фазы в растровых строках.
|
# MAME = время фазы в растровых строках.
|
||||||
PROF ?= 1
|
PROF ?= 1
|
||||||
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||||
EXTRA_SRCS := pop_state.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
|
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 \
|
||||||
@@ -58,7 +83,7 @@ hdd: $(EXAMPLE).exe
|
|||||||
$(foreach f,$(BG_DATA),BG:$(f)) \
|
$(foreach f,$(BG_DATA),BG:$(f)) \
|
||||||
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
||||||
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
||||||
LEVELS:$(LVL_DATA)
|
$(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
|
||||||
@@ -71,4 +96,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
|||||||
kid_data.h $(KID_BIN): $(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: guards.c pop_guard.c pop_guard.h pop_gdraw.c 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)
|
$(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 |
@@ -10,17 +10,493 @@
|
|||||||
* пока исполняется банк, резидентной страницы в адресном пространстве
|
* пока исполняется банк, резидентной страницы в адресном пространстве
|
||||||
* нет. Поэтому фон/окклюзию за стража рисует главный цикл.
|
* нет. Поэтому фон/окклюзию за стража рисует главный цикл.
|
||||||
*
|
*
|
||||||
* Сейчас — каркас: тик есть, ИИ и боёвка (порт seg005 control_guard*,
|
* Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000
|
||||||
* seg004 fight/collision) добавляются сюда.
|
* play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой:
|
||||||
|
* ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а
|
||||||
|
* дальше работает общий pop_control().
|
||||||
*/
|
*/
|
||||||
#include "pop_guard.h"
|
#include "pop_guard.h"
|
||||||
#include "pop_kid.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
|
void pop_guard_tick(void) __banked
|
||||||
{
|
{
|
||||||
if (Guard.charid == 0) return; /* стража в комнате нет */
|
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, что у Кида, но своя таблица кадров —
|
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
|
||||||
* выбирается по Char.charid внутри load_frame (seg006:0293). */
|
* выбирается по Char.charid внутри load_frame (seg006:0293). */
|
||||||
pop_guard_play();
|
play_seq();
|
||||||
/* TODO: порт control_guard/fight (seg005/seg004) — состояние в Guard. */
|
pop_saveshad();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,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,10 +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_state.h"
|
||||||
#include "pop_geom.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];
|
||||||
@@ -135,18 +137,12 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
|
|||||||
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, а
|
||||||
@@ -354,7 +350,7 @@ 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;
|
||||||
|
|
||||||
#define prandom(maxv) pop_prandom(&rnd_seed, (maxv))
|
#define prandom(maxv) pop_prandom(&rnd_seed, (maxv))
|
||||||
|
|
||||||
@@ -417,7 +413,7 @@ static void wall_rnd(int row, int col)
|
|||||||
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
|
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
|
||||||
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
|
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
|
||||||
uint8_t a, b, bg;
|
uint8_t a, b, bg;
|
||||||
unsigned long saved;
|
pop_rnd_t saved;
|
||||||
if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
|
if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
|
||||||
a = wp_a[idx]; b = wp_b[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_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7);
|
||||||
@@ -430,7 +426,7 @@ static void wall_rnd(int row, int col)
|
|||||||
saved = rnd_seed;
|
saved = rnd_seed;
|
||||||
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
|
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
|
||||||
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 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);
|
pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col);
|
||||||
prandom(1); /* сброс */
|
prandom(1); /* сброс */
|
||||||
wr_mid_div = (uint8_t)prandom(1);
|
wr_mid_div = (uint8_t)prandom(1);
|
||||||
wr_mid_off = (uint8_t)prandom(4);
|
wr_mid_off = (uint8_t)prandom(4);
|
||||||
@@ -590,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); /* лестница (комната ≠ стартовой) */
|
||||||
@@ -598,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,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);
|
||||||
|
|
||||||
@@ -839,8 +847,22 @@ void pop_ceil_bake_empty(int col) __banked
|
|||||||
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);
|
||||||
@@ -919,6 +941,35 @@ void pop_leveldoor_redraw(int row, int col) __banked
|
|||||||
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 —
|
||||||
@@ -983,6 +1034,7 @@ 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];
|
||||||
|
|
||||||
@@ -1011,6 +1063,7 @@ void pop_loose_mob_spawn(int row, int col) __banked
|
|||||||
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;
|
||||||
@@ -1044,14 +1097,42 @@ void pop_loose_mob_reset(void) __banked
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Смена комнаты. Куски НЕ останавливаем: в оригинале 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;
|
||||||
}
|
}
|
||||||
@@ -1064,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) {
|
||||||
/* сигнал прошлого приземления ещё не разобран (один за кадр) —
|
/* сигнал прошлого приземления ещё не разобран (один за кадр) —
|
||||||
* подержим кусок на границе и приземлим следующим кадром */
|
* подержим кусок на границе и приземлим следующим кадром */
|
||||||
@@ -1079,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: кто
|
||||||
@@ -1162,8 +1252,13 @@ void pop_loose_mob_draw_over(void) __banked
|
|||||||
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1190,6 +1285,11 @@ 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);/* статус-полоса + нижний борт */
|
||||||
}
|
}
|
||||||
@@ -1466,7 +1566,7 @@ static void other_overlay_tile(int row, int col)
|
|||||||
static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw;
|
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,
|
static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
int8_t dir)
|
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-база) */
|
||||||
@@ -1477,6 +1577,14 @@ static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
|||||||
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. Верхний ряд считается от
|
||||||
@@ -1508,7 +1616,7 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
|||||||
{
|
{
|
||||||
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
|
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
|
||||||
fclip_on = 1; /* окно = прямоугольник спрайта */
|
fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||||
char_footprint(obj_x, obj_y, w, h, dir);
|
char_footprint(obj_x, obj_y, w, h, dir, Kid.sword);
|
||||||
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
|
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
|
||||||
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
|
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
|
||||||
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
|
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
|
||||||
@@ -1592,11 +1700,11 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
|||||||
* (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
|
* (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
|
||||||
* порт redraw_at_char2, а не догадки сейчас. */
|
* порт redraw_at_char2, а не догадки сейчас. */
|
||||||
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
int8_t dir)
|
int8_t dir, uint8_t sword) __banked
|
||||||
{
|
{
|
||||||
int8_t r, c;
|
int8_t r, c;
|
||||||
fclip_on = 1; /* окно = прямоугольник спрайта */
|
fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||||
char_footprint(obj_x, obj_y, w, h, dir);
|
char_footprint(obj_x, obj_y, w, h, dir, sword);
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
for (r = fp_rT; r <= fp_rB; r++)
|
for (r = fp_rT; r <= fp_rB; r++)
|
||||||
for (c = fp_cL; c <= fp_cR; c++)
|
for (c = fp_cL; c <= fp_cR; c++)
|
||||||
|
|||||||
@@ -25,6 +25,11 @@
|
|||||||
/* Экранный 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 — ошибка. */
|
||||||
@@ -41,9 +46,11 @@ 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) __banked;
|
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] — колонки комнаты снизу,
|
||||||
|
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
|
||||||
|
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
|
||||||
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
|
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
|
||||||
|
|
||||||
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
||||||
@@ -69,8 +76,11 @@ void pop_fore_set_clip(int x, int y, int w, int h) __banked;
|
|||||||
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
|
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
|
||||||
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
|
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
|
||||||
* (до ×8/7), как у kid_fp_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,
|
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||||
int8_t dir);
|
int8_t dir, uint8_t sword) __banked;
|
||||||
|
|
||||||
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
|
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
|
||||||
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
|
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
|
||||||
@@ -120,12 +130,22 @@ void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кно
|
|||||||
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
|
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
|
||||||
void pop_leveldoor_redraw(int row, int col) __banked;
|
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) __banked;
|
void pop_loose_mob_spawn(int row, int col) __banked;
|
||||||
void pop_loose_mob_tick(void) __banked;
|
void pop_loose_mob_tick(void) __banked;
|
||||||
void pop_loose_mob_reset(void) __banked; /* остановить кусок (смена комнаты) */
|
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-слоем. */
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* просто глобальный флаг: main ставит его при старте (на время разработки —
|
* просто глобальный флаг: main ставит его при старте (на время разработки —
|
||||||
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
|
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
|
||||||
*
|
*
|
||||||
* Реализовано: K — убить стража (seg000:786). Остальные читы оригинала
|
* Реализовано: K — убить стража (seg000:786), I/S/Shift+L (наши отладочные),
|
||||||
|
* [ / ] — подгонка Кида по X (seg000:1828). Остальные читы оригинала
|
||||||
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
|
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
|
||||||
*/
|
*/
|
||||||
#ifndef POP_CHEAT_H
|
#ifndef POP_CHEAT_H
|
||||||
@@ -17,5 +18,13 @@
|
|||||||
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
|
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
|
||||||
|
|
||||||
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
|
#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
|
#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,15 +74,15 @@
|
|||||||
#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: в оригинале это глобалы, через которые ИИ стража подаёт
|
* флаг сборки, а не свойство кода: включат --bank-data ради экономии
|
||||||
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же
|
* W1/W2 — и шина молча уедет в страницу банка.
|
||||||
* control(), что и у игрока. Поэтому шина общая. */
|
*
|
||||||
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
|
* 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 ------------ *
|
||||||
@@ -125,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-запросы) — это
|
|
||||||
* полировка K3; K2b просто запускает run-jump. */
|
|
||||||
if (Char.frame >= FRAME_7_RUN) {
|
if (Char.frame >= FRAME_7_RUN) {
|
||||||
|
if (!pop_run_jump_align()) return;
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -179,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
|
||||||
@@ -232,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();
|
||||||
@@ -324,10 +370,199 @@ static void control_jumpup(void)
|
|||||||
standing_jump();
|
standing_jump();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */
|
/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
|
||||||
void pop_control(void)
|
* клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
|
||||||
|
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
|
||||||
|
* уже как выпад. */
|
||||||
|
static void draw_sword(void)
|
||||||
|
{
|
||||||
|
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;
|
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/плита по голове) нажатием ↑, и
|
||||||
@@ -338,7 +573,13 @@ void pop_control(void)
|
|||||||
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();
|
||||||
@@ -434,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;
|
||||||
@@ -442,27 +683,45 @@ 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();
|
||||||
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
|
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
|
||||||
* персонажем (как в оригинале), поэтому ввод игрока применяется к
|
* персонажем (как в оригинале), поэтому ввод игрока применяется к
|
||||||
* Киду только внутри loadkid/savekid. Кадр не перезагружаем —
|
* Киду только внутри loadkid/savekid.
|
||||||
* play_seq в kid_tick сделает это следующим шагом. */
|
*
|
||||||
|
* 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_loadkid();
|
||||||
|
pop_load_fram_det_col();
|
||||||
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
|
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
|
||||||
pop_savekid_state();
|
pop_savekid_state();
|
||||||
save_ctrl_1();
|
save_ctrl_1();
|
||||||
|
|||||||
@@ -18,19 +18,26 @@
|
|||||||
|
|
||||||
#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) ----
|
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
|
||||||
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
|
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
|
||||||
@@ -39,12 +46,23 @@ int8_t pop_ctrl_shift_held(void);
|
|||||||
#define POP_CONTROL_RELEASED 0
|
#define POP_CONTROL_RELEASED 0
|
||||||
#define POP_CONTROL_IGNORE 1
|
#define POP_CONTROL_IGNORE 1
|
||||||
#define POP_CONTROL_HELD (-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,
|
extern int8_t control_forward, control_backward, control_up, control_down,
|
||||||
control_shift2;
|
control_shift2;
|
||||||
|
|
||||||
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
|
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
|
||||||
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из
|
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из
|
||||||
* своего тика напрямую. */
|
* своего тика напрямую. */
|
||||||
void pop_control(void);
|
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);
|
||||||
|
}
|
||||||
@@ -15,7 +15,10 @@
|
|||||||
#include <gfx.h>
|
#include <gfx.h>
|
||||||
#include <sprite.h>
|
#include <sprite.h>
|
||||||
#include "pop_guard.h"
|
#include "pop_guard.h"
|
||||||
|
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
|
||||||
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
|
#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"
|
#include "kid_data.h"
|
||||||
|
|
||||||
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
|
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
|
||||||
@@ -29,6 +32,21 @@ static int gx_l[2], gy_l[2];
|
|||||||
static uint16_t gw_l[2], gh_l[2];
|
static uint16_t gw_l[2], gh_l[2];
|
||||||
static uint8_t gvalid[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
|
int pop_guard_load(void) __banked
|
||||||
{
|
{
|
||||||
static const char *const names[GUARD_PAGES] = {
|
static const char *const names[GUARD_PAGES] = {
|
||||||
@@ -50,19 +68,28 @@ void pop_guard_free(void) __banked
|
|||||||
g_ok = 0;
|
g_ok = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же
|
||||||
|
* причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в
|
||||||
|
* большинстве кадров noclip-ядром. Два прямоугольника, а не объединение
|
||||||
|
* (см. комментарий к sx_l выше). */
|
||||||
void pop_guard_heal(void) __banked
|
void pop_guard_heal(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t p = gfx_get_draw_page() & 1;
|
uint8_t p = gfx_get_draw_page() & 1;
|
||||||
if (gvalid[p]) gfx_heal(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
|
if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
|
||||||
gvalid[p] = 0;
|
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
|
void pop_guard_draw(void) __banked
|
||||||
{
|
{
|
||||||
int obj_x, obj_y, top, fwd, fp_x, bx0 = 0, top0 = 0;
|
int obj_x, obj_y, top, fwd, fp_x;
|
||||||
uint8_t page, idx, p, flip;
|
uint8_t page, idx, p, flip;
|
||||||
const uint8_t *img;
|
const uint8_t *img;
|
||||||
uint16_t w, h;
|
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;
|
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
|
||||||
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
|
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
|
||||||
@@ -85,22 +112,126 @@ void pop_guard_draw(void) __banked
|
|||||||
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
|
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
|
||||||
if (flip) bx -= (int)w;
|
if (flip) bx -= (int)w;
|
||||||
top = obj_y - (int)h + 1;
|
top = obj_y - (int)h + 1;
|
||||||
bx0 = bx; top0 = top;
|
|
||||||
p = gfx_get_draw_page() & 1;
|
p = gfx_get_draw_page() & 1;
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
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_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
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;
|
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();
|
gfx_w0_unmap();
|
||||||
/* Окно fore-слоя = прямоугольник стража (см. pop_bg.c, fclip_on):
|
/* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
|
||||||
* без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов на кадр. */
|
* fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */
|
||||||
pop_fore_set_clip(bx0, top0 + POP_YOFF, (int)w, (int)h);
|
pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
|
||||||
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
|
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
|
||||||
* midtable, а foretable рисуется после всех midtable-записей, поэтому
|
* midtable, а foretable рисуется после всех midtable-записей, поэтому
|
||||||
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
|
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
|
||||||
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
|
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
|
||||||
* столб. */
|
* столб. */
|
||||||
if (w && h)
|
if (w && h)
|
||||||
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,222 @@ int8_t pop_y_to_row(int16_t y)
|
|||||||
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
|
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv)
|
/* Уложить значение в диапазон 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)
|
||||||
{
|
{
|
||||||
unsigned long s = *seed * 214013UL + 2531011UL;
|
uint16_t n = (uint16_t)(maxv + 1);
|
||||||
*seed = s;
|
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
|
||||||
return (uint16_t)((uint16_t)(s >> 16) % (uint16_t)(maxv + 1));
|
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
|
||||||
|
|||||||
@@ -26,9 +26,36 @@ extern const int16_t pop_y_land[5];
|
|||||||
* над комнатой, 3 = под комнатой). */
|
* над комнатой, 3 = под комнатой). */
|
||||||
int8_t pop_y_to_row(int16_t y);
|
int8_t pop_y_to_row(int16_t y);
|
||||||
|
|
||||||
/* prandom (seg009:321) — 32-битный LCG оригинала. Сид ВНЕШНИЙ: у стен
|
/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
|
||||||
* (раскладка кладки) и у анимаций тайлов (фаза факелов) свои
|
* анимаций тайлов (фаза факелов) свои последовательности, смешивать их
|
||||||
* последовательности, смешивать их нельзя. */
|
* нельзя.
|
||||||
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv);
|
*
|
||||||
|
* ДВА генератора, выбор через 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
|
#endif
|
||||||
|
|||||||
@@ -10,11 +10,22 @@
|
|||||||
#include "pop_geom.h"
|
#include "pop_geom.h"
|
||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
|
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
|
||||||
|
#include "pop_map.h" /* hitp_* — HP Кида */
|
||||||
#include "kid_data.h"
|
#include "kid_data.h"
|
||||||
|
|
||||||
pop_char_t Guard;
|
pop_char_t Guard;
|
||||||
uint8_t guardhp_curr, guardhp_max;
|
uint8_t guardhp_curr, guardhp_max;
|
||||||
int8_t guardhp_delta;
|
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). Глобальный, а не статик: его
|
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
|
||||||
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
|
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
|
||||||
@@ -32,6 +43,10 @@ void pop_guard_reset(void)
|
|||||||
Guard.direction = 0;
|
Guard.direction = 0;
|
||||||
guardhp_curr = 0;
|
guardhp_curr = 0;
|
||||||
guardhp_delta = 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):
|
/* kill guard cheat (seg000:786):
|
||||||
@@ -44,11 +59,35 @@ void pop_guard_kill(void)
|
|||||||
{
|
{
|
||||||
if (Guard.charid == CHARID_4_SKELETON) return;
|
if (Guard.charid == CHARID_4_SKELETON) return;
|
||||||
if (!pop_guard_present()) return;
|
if (!pop_guard_present()) return;
|
||||||
|
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
|
||||||
|
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
|
||||||
|
* общий control() ставит ему seq_71_dying. */
|
||||||
guardhp_delta = (int8_t)-guardhp_curr;
|
guardhp_delta = (int8_t)-guardhp_curr;
|
||||||
guardhp_curr = 0;
|
|
||||||
Guard.alive = 0;
|
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): таблица у
|
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
|
||||||
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где
|
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где
|
||||||
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||||||
@@ -61,11 +100,39 @@ void pop_guard_load_frame(void)
|
|||||||
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
|
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)
|
void pop_guard_enter(uint8_t room)
|
||||||
{
|
{
|
||||||
uint8_t tile, color, skill;
|
uint8_t tile, color, skill;
|
||||||
int8_t dir;
|
int8_t dir;
|
||||||
pop_guard_reset();
|
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;
|
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
|
||||||
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
|
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
|
||||||
* колонки тайла, а не берётся из файла. */
|
* колонки тайла, а не берётся из файла. */
|
||||||
@@ -80,13 +147,44 @@ void pop_guard_enter(uint8_t room)
|
|||||||
Guard.alive = -1;
|
Guard.alive = -1;
|
||||||
Guard.action = 1; /* actions_1_run_jump */
|
Guard.action = 1; /* actions_1_run_jump */
|
||||||
Guard.fall_x = Guard.fall_y = 0;
|
Guard.fall_x = Guard.fall_y = 0;
|
||||||
guardhp_max = guardhp_curr = 3; /* get_guard_hp: skill 0, уровень 1 */
|
/* 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;
|
guardhp_delta = 0;
|
||||||
/* enter_guard (seg002:0208): seqtbl_offset_char(seq_77_guard_stand_inactive).
|
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
|
||||||
* Последовательность крутит общий интерпретатор (pop_guard_play), а
|
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
|
||||||
* таблицу кадров он выбирает по Char.charid — у стража своя. */
|
* ровно там, где лёг (порт ветки 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();
|
pop_loadshad();
|
||||||
pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
|
if (saved_seq) Char.curr_seq = saved_seq;
|
||||||
|
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
|
||||||
pop_saveshad();
|
pop_saveshad();
|
||||||
pop_guard_play(); /* до первого кадра */
|
pop_guard_play(); /* до первого кадра */
|
||||||
|
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
|
||||||
|
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
|
||||||
|
Guard.alive = 1;
|
||||||
|
guardhp_curr = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,45 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "kid_data.h" /* kframe */
|
#include "kid_data.h" /* kframe */
|
||||||
#include "pop_char.h"
|
#include "pop_char.h"
|
||||||
|
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
|
||||||
|
|
||||||
/* seqids (types.h:1070) — последовательности стража. */
|
/* 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_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) — нужны хотя бы для «скелет не убивается читом». */
|
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
|
||||||
#define CHARID_0_KID 0
|
#define CHARID_0_KID 0
|
||||||
@@ -31,12 +67,53 @@
|
|||||||
#define CHARID_4_SKELETON 4
|
#define CHARID_4_SKELETON 4
|
||||||
#define CHARID_5_PRINCESS 5
|
#define CHARID_5_PRINCESS 5
|
||||||
|
|
||||||
|
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
|
||||||
|
#define NUM_GUARD_SKILLS 12
|
||||||
|
|
||||||
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
|
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
|
||||||
|
|
||||||
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
|
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
|
||||||
extern uint8_t guardhp_curr, guardhp_max;
|
extern uint8_t guardhp_curr, guardhp_max;
|
||||||
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
|
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,
|
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
|
||||||
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
||||||
extern kframe pop_gframe;
|
extern kframe pop_gframe;
|
||||||
@@ -54,12 +131,27 @@ void pop_guard_free(void) __banked;
|
|||||||
* seg002:0112) либо убрать его, если в комнате стража нет. */
|
* seg002:0112) либо убрать его, если в комнате стража нет. */
|
||||||
void pop_guard_enter(uint8_t room);
|
void pop_guard_enter(uint8_t room);
|
||||||
|
|
||||||
|
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
|
||||||
|
* enter_room, пока Guard ещё держит прежнего. */
|
||||||
|
void pop_guard_leave(void);
|
||||||
|
|
||||||
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
|
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
|
||||||
* как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c
|
* как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c
|
||||||
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
|
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
|
||||||
|
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
|
||||||
|
* ПОСЛЕ персонажей — она ничего не перекрывает. */
|
||||||
|
void pop_hp_draw(void) __banked;
|
||||||
|
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
|
||||||
|
void pop_hp_invalidate(void) __banked;
|
||||||
|
|
||||||
void pop_guard_heal(void) __banked;
|
void pop_guard_heal(void) __banked;
|
||||||
void pop_guard_draw(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);
|
void pop_guard_reset(void);
|
||||||
|
|
||||||
@@ -67,6 +159,10 @@ void pop_guard_reset(void);
|
|||||||
* при включённых читах — проверку делает вызывающий. */
|
* при включённых читах — проверку делает вызывающий. */
|
||||||
void pop_guard_kill(void);
|
void pop_guard_kill(void);
|
||||||
|
|
||||||
|
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
|
||||||
|
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */
|
||||||
|
void pop_do_delta_hp(void);
|
||||||
|
|
||||||
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
|
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
|
||||||
void pop_guard_tick(void) __banked;
|
void pop_guard_tick(void) __banked;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "pop_kid.h"
|
#include "pop_kid.h"
|
||||||
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
|
#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 "pop_geom.h"
|
||||||
#include "kid_data.h"
|
#include "kid_data.h"
|
||||||
@@ -22,21 +23,28 @@ pop_char_t Kid;
|
|||||||
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
||||||
int8_t knock;
|
int8_t knock;
|
||||||
|
|
||||||
|
/* Порт глобала next_level: seqtbl-команда END_LEVEL инкрементит его в конце
|
||||||
|
* seq_70 (подъём по лестнице за дверью уровня), а главный цикл сравнивает с
|
||||||
|
* текущим уровнем и грузит следующий. Счётчик, а не флаг — ровно как в
|
||||||
|
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
|
||||||
|
* легли сюда же без переделки. */
|
||||||
|
uint8_t pop_next_level;
|
||||||
|
|
||||||
|
|
||||||
/* ---- Атласы 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;
|
||||||
|
|
||||||
@@ -47,6 +55,74 @@ int pop_sword_load(void)
|
|||||||
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] = {
|
||||||
@@ -132,13 +208,61 @@ static kframe kid_frame; /* кадр Kid для kid_draw */
|
|||||||
* копирование платится один раз на переключение персонажа. */
|
* копирование платится один раз на переключение персонажа. */
|
||||||
pop_char_t Char;
|
pop_char_t Char;
|
||||||
|
|
||||||
void pop_loadkid(void) { Char = Kid; }
|
/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
|
||||||
void pop_savekid(void) { Kid = Char; kid_frame = cur_frame; }
|
* рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
|
||||||
/* Выгрузка БЕЗ кадра: control() кадр не трогает, а cur_frame в этот
|
* ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
|
||||||
* момент принадлежит тому, кто последним крутил play_seq. */
|
* ними: для стража Opp = Kid, для Кида Opp = Guard. */
|
||||||
void pop_savekid_state(void) { Kid = Char; }
|
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_loadshad(void) { Char = Guard; }
|
||||||
void pop_saveshad(void) { Guard = Char; pop_gframe = cur_frame; }
|
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)
|
int pop_kid_data_load(const char *path)
|
||||||
{
|
{
|
||||||
@@ -215,6 +339,15 @@ static void load_frame(void)
|
|||||||
cur_frame.dy = (int8_t)f[2];
|
cur_frame.dy = (int8_t)f[2];
|
||||||
cur_frame.flags = f[3];
|
cur_frame.flags = f[3];
|
||||||
cur_frame.sword = f[4];
|
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)
|
||||||
@@ -226,6 +359,7 @@ static uint8_t char_dx_forward(int8_t dx)
|
|||||||
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
|
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
|
||||||
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
|
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
|
||||||
int8_t kid_cur_dx(void) { return cur_frame.dx; }
|
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; }
|
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
|
||||||
|
|
||||||
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
|
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
|
||||||
@@ -289,10 +423,21 @@ 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): звука у нас нет, но опкод несёт
|
||||||
|
* ВТОРОЙ смысл — «Кид нашумел», и его терять нельзя.
|
||||||
|
* Шумом считаются 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++;
|
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) */
|
||||||
@@ -311,6 +456,40 @@ void play_seq(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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: тот
|
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
|
||||||
* работает с Char и сам по себе не знает, чей это персонаж. */
|
* работает с Char и сам по себе не знает, чей это персонаж. */
|
||||||
@@ -327,6 +506,7 @@ void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
|
|||||||
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.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];
|
||||||
pop_kid_play(); /* до первого кадра */
|
pop_kid_play(); /* до первого кадра */
|
||||||
}
|
}
|
||||||
@@ -363,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;
|
||||||
@@ -406,38 +644,38 @@ 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);
|
||||||
|
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_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);
|
|
||||||
/* Окно fore-слоя расширяем тем же объединением: брызги
|
|
||||||
* тоже должны уходить под передние грани тайлов. */
|
|
||||||
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, 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)
|
||||||
@@ -482,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;
|
||||||
@@ -495,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);
|
||||||
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
|
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
|
||||||
* узко, только по прямоугольнику Кида и только при выходе. */
|
* узко, только по прямоугольнику Кида и только при выходе. */
|
||||||
@@ -505,62 +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-слоя = итоговый прямоугольник спрайта (с клинком и
|
/* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
|
||||||
* обрезкой clip_char): fore-проход вернёт куски тайлов ТОЛЬКО поверх
|
* fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
|
||||||
* него, а не целыми тайлами — см. pop_bg.c, fclip_on. Ставим и когда
|
* тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
|
||||||
* рисовать нечего (kid_lvalid=0): нулевое окно = fore-проход не
|
* нулевое окно = fore-проход не трогает экран, что и правильно. */
|
||||||
* трогает экран, что и правильно — перекрывать нечего. */
|
kid_fore_clip(gfx_get_draw_page() & 1);
|
||||||
{
|
}
|
||||||
uint8_t dp = gfx_get_draw_page() & 1;
|
|
||||||
if (kid_lvalid[dp])
|
/* Вернуть окно fore-клипа на прямоугольник ПОСЛЕДНЕГО кадра Kid.
|
||||||
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
|
*
|
||||||
(int)kid_lw[dp], (int)kid_lh[dp]);
|
* Окно (pop_fore_set_clip) — одно на всех, и его ставит КАЖДЫЙ, кто рисует
|
||||||
else
|
* персонажа: kid_draw в конце — своё, pop_guard_draw в конце — своё. Пока
|
||||||
pop_fore_set_clip(0, 0, 0, 0);
|
* страж рисовался строго ДО Кида, к моменту 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "pop_char.h"
|
#include "pop_char.h"
|
||||||
|
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
|
||||||
|
|
||||||
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
|
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
|
||||||
extern pop_char_t Kid;
|
extern pop_char_t Kid;
|
||||||
@@ -20,6 +21,11 @@ extern pop_char_t Kid;
|
|||||||
* (свой ряд). Ставит 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);
|
||||||
@@ -43,6 +49,15 @@ void pop_kid_data_free(void);
|
|||||||
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);
|
||||||
@@ -59,11 +74,22 @@ void play_seq(void); /* работает с Char — см. pop_kid_play
|
|||||||
* Интерпретатор последовательностей общий для Kid и стража: у них общая
|
* Интерпретатор последовательностей общий для Kid и стража: у них общая
|
||||||
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
|
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
|
||||||
extern pop_char_t Char;
|
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_loadkid(void);
|
||||||
void pop_savekid(void);
|
void pop_savekid(void);
|
||||||
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
|
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
|
||||||
void pop_loadshad(void);
|
void pop_loadshad(void);
|
||||||
void pop_saveshad(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 +
|
/* Прокрутить последовательность персонажа целиком (load + play_seq +
|
||||||
* save). Звать вместо голого play_seq. */
|
* save). Звать вместо голого play_seq. */
|
||||||
@@ -71,9 +97,17 @@ void pop_char_set_seq(uint8_t seq_id); /* seq активного персон
|
|||||||
void pop_kid_play(void);
|
void pop_kid_play(void);
|
||||||
void pop_guard_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]) — единая
|
||||||
@@ -92,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
|
||||||
@@ -27,12 +33,18 @@ extern void _gfx_w0_isr(void);
|
|||||||
#define BP_GTILE 2119u
|
#define BP_GTILE 2119u
|
||||||
#define BP_GDIR 2143u
|
#define BP_GDIR 2143u
|
||||||
#define BP_GX 2167u
|
#define BP_GX 2167u
|
||||||
|
#define BP_GSEQLO 2191u
|
||||||
#define BP_GSKILL 2215u
|
#define BP_GSKILL 2215u
|
||||||
|
#define BP_GSEQHI 2239u
|
||||||
#define BP_GCOLOR 2263u
|
#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];
|
||||||
@@ -68,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,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)
|
||||||
@@ -148,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();
|
||||||
@@ -177,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; }
|
||||||
@@ -243,22 +358,95 @@ static uint8_t start_byte(uint16_t off)
|
|||||||
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
|
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
|
||||||
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
|
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
|
||||||
* колонки (pos_guards), поэтому guards_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 pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||||
uint8_t *color, uint8_t *skill)
|
uint8_t *color, uint8_t *skill)
|
||||||
{
|
{
|
||||||
const uint8_t *p;
|
const uint8_t *g;
|
||||||
uint8_t t;
|
uint8_t t;
|
||||||
if (!lvl_ok || room < 1 || room > 24) return 0;
|
if (!lvl_ok || room < 1 || room > 24) return 0;
|
||||||
gfx_w0_map(lvl_page);
|
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
|
||||||
p = (const uint8_t *)LVL_DATA_OFF;
|
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
|
||||||
t = p[BP_GTILE + room - 1];
|
g = gstate + (uint16_t)(room - 1) * 6;
|
||||||
|
t = g[GS_TILE];
|
||||||
if (t < 30) {
|
if (t < 30) {
|
||||||
*tile = t;
|
*tile = t;
|
||||||
*dir = (int8_t)p[BP_GDIR + room - 1];
|
*dir = (int8_t)g[GS_DIR];
|
||||||
*color = (uint8_t)(p[BP_GCOLOR + room - 1] & 0x0F);
|
*skill = g[GS_SKILL];
|
||||||
*skill = p[BP_GSKILL + room - 1];
|
gfx_w0_map(lvl_page);
|
||||||
}
|
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
|
||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
return (uint8_t)(t < 30);
|
return (uint8_t)(t < 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -75,6 +120,18 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col);
|
|||||||
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
|
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
|
||||||
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
|
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
|
||||||
* Порт enter_guard (seg002:0112). */
|
* Порт 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 pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||||
uint8_t *color, uint8_t *skill);
|
uint8_t *color, uint8_t *skill);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
#include "pop_bg.h"
|
#include "pop_bg.h"
|
||||||
#include "pop_state.h"
|
#include "pop_state.h"
|
||||||
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
|
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
|
||||||
|
#include "pop_level.h" /* pop_level_start_room — вход в дверь уровня */
|
||||||
|
#include "pop_guard.h" /* SWORD_2_DRAWN — ветка приземления с мечом */
|
||||||
|
#include "pop_tune.h" /* POP_ENABLE_JUMP_GRAB и прочие настройки */
|
||||||
|
|
||||||
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
|
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
|
||||||
#define TILE_EMPTY 0
|
#define TILE_EMPTY 0
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
#define TILE_POTION 10 /* зелье */
|
#define TILE_POTION 10 /* зелье */
|
||||||
#define TILE_SWORD 22 /* меч на полу */
|
#define TILE_SWORD 22 /* меч на полу */
|
||||||
#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */
|
#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */
|
||||||
|
#define TILE_LEVELDOOR_LEFT 16 /* ЛЕВАЯ половина двери уровня (на ней modif) */
|
||||||
#define ACT_STAND 0
|
#define ACT_STAND 0
|
||||||
#define ACT_RUNJUMP 1
|
#define ACT_RUNJUMP 1
|
||||||
#define ACT_HANGCLIMB 2
|
#define ACT_HANGCLIMB 2
|
||||||
@@ -66,6 +70,7 @@
|
|||||||
#define SEQ_47_BUMP 47 /* удар в стену: dx(-4) отскок + 50-52 + stand */
|
#define SEQ_47_BUMP 47 /* удар в стену: dx(-4) отскок + 50-52 + stand */
|
||||||
/* K4 — зацеп/вис/подтягивание/спуск */
|
/* K4 — зацеп/вис/подтягивание/спуск */
|
||||||
#define SEQ_8_GRAB_STRAIGHT 8 /* прыжок вверх и зацеп (прямо) */
|
#define SEQ_8_GRAB_STRAIGHT 8 /* прыжок вверх и зацеп (прямо) */
|
||||||
|
#define SEQ_9_GRAB_WHILE_JUMPING 9 /* зацеп ПРЯМО В ПРЫЖКЕ (улучшение SDLPoP) */
|
||||||
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
|
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
|
||||||
#define SEQ_16_JUMP_UP_AND_GRAB 16 /* прыжок вверх и зацеп (нет пола сзади)*/
|
#define SEQ_16_JUMP_UP_AND_GRAB 16 /* прыжок вверх и зацеп (нет пола сзади)*/
|
||||||
#define SEQ_24_GRAB_FORWARD 24 /* прыжок вверх и зацеп (пол сзади) */
|
#define SEQ_24_GRAB_FORWARD 24 /* прыжок вверх и зацеп (пол сзади) */
|
||||||
@@ -78,6 +83,8 @@
|
|||||||
#define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */
|
#define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */
|
||||||
#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */
|
#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */
|
||||||
#define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */
|
#define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */
|
||||||
|
#define SEQ_63_ACTIVE_AFTER_FALL 63 /* стойка С МЕЧОМ после приземления */
|
||||||
|
#define SEQ_81_FIGHTFALL 81 /* fightfall: падение из боевой стойки */
|
||||||
|
|
||||||
#define TILE_FLOOR 1
|
#define TILE_FLOOR 1
|
||||||
#define FRAME_109_CROUCH 109
|
#define FRAME_109_CROUCH 109
|
||||||
@@ -114,17 +121,29 @@ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавше
|
|||||||
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
|
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
|
||||||
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
|
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
|
||||||
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
|
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
|
||||||
|
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
|
||||||
|
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
|
||||||
|
* Смерть/рестарт уровня возвращают ровно его (do_startpos: hitp_max =
|
||||||
|
* hitp_curr = hitp_beg_lev), а по ПРОХОЖДЕНИИ уровня он подтягивается к
|
||||||
|
* достигнутому максимуму (hitp_beg_lev = hitp_max) — так «жизнь» из большой
|
||||||
|
* склянки переносится на следующий уровень, а из мёртвых прогонов — нет.
|
||||||
|
* Стартовое значение (custom->start_hitp = 3) ставит main через
|
||||||
|
* pop_kid_hp_newgame — инициализатором нельзя: модуль банкованный. */
|
||||||
|
uint8_t hitp_beg_lev;
|
||||||
|
/* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки
|
||||||
|
* (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале.
|
||||||
|
* Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и
|
||||||
|
* проверен до боёвки, и переводить его на дельту без нужды рискованно. */
|
||||||
|
int8_t hitp_delta;
|
||||||
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
|
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
|
||||||
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
|
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
|
||||||
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
|
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
|
||||||
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
|
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
|
||||||
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
|
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
|
||||||
/* Состояние edge-триггера бампа (порт prev_coll_flags, seg004): колонка, для
|
/* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам
|
||||||
* которой сохранены флаги перекрытия, и сами флаги (бит0 = левая грань стены,
|
* (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом
|
||||||
* бит1 = правая). BUMP_COL_NONE — данных нет (смена комнаты/старт). */
|
* с check_collisions. Сброс при смене комнаты — coll_prev_row. */
|
||||||
#define BUMP_COL_NONE ((int8_t)-128)
|
static void pop_coll_invalidate(void);
|
||||||
static int8_t bump_prev_col;
|
|
||||||
static uint8_t bump_prev_flags;
|
|
||||||
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
|
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
|
||||||
* соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично
|
* соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично
|
||||||
* (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах —
|
* (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах —
|
||||||
@@ -156,7 +175,27 @@ void pop_map_set(uint8_t *fg) __banked { g_fg = fg; }
|
|||||||
|
|
||||||
void pop_map_set_room(uint8_t room) __banked { g_room = room; }
|
void pop_map_set_room(uint8_t room) __banked { g_room = room; }
|
||||||
|
|
||||||
void pop_kid_hp_reset(void) __banked { hitp_curr = 3; pop_kid_dead = 0; }
|
/* do_startpos (seg003:0160): HP уровня = hitp_beg_lev, а не константа —
|
||||||
|
* иначе большая склянка теряется на границе уровня. */
|
||||||
|
void pop_kid_hp_reset(void) __banked
|
||||||
|
{
|
||||||
|
hitp_curr = hitp_max = hitp_beg_lev;
|
||||||
|
hitp_delta = 0;
|
||||||
|
pop_kid_dead = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */
|
||||||
|
void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; }
|
||||||
|
|
||||||
|
/* add_life (seg000:11EC) + set_health_life (seg000:1200): большая склянка
|
||||||
|
* поднимает ПОТОЛОК HP (до max_hitp_allowed = 10) и доливает до него. */
|
||||||
|
static void pop_kid_add_life(void)
|
||||||
|
{
|
||||||
|
uint8_t hpmax = (uint8_t)(hitp_max + 1);
|
||||||
|
if (hpmax > POP_MAX_HITP) hpmax = POP_MAX_HITP;
|
||||||
|
hitp_max = hpmax;
|
||||||
|
hitp_delta = (int8_t)(hitp_max - hitp_curr);
|
||||||
|
}
|
||||||
|
|
||||||
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
|
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
|
||||||
static uint8_t take_hp(uint8_t count)
|
static uint8_t take_hp(uint8_t count)
|
||||||
@@ -175,7 +214,7 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
|
|||||||
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
|
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
|
||||||
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и
|
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и
|
||||||
* бампа на кадре входа нет). */
|
* бампа на кадре входа нет). */
|
||||||
bump_prev_col = BUMP_COL_NONE;
|
pop_coll_invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t get_tile(int8_t col, int8_t row)
|
static uint8_t get_tile(int8_t col, int8_t row)
|
||||||
@@ -202,6 +241,16 @@ static uint8_t get_tile(int8_t col, int8_t row)
|
|||||||
return g_curr_tile;
|
return g_curr_tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Тайл ТЕКУЩЕЙ комнаты по клетке — наружу (порт get_tile(room,col,row) для
|
||||||
|
* случая room == текущая). Нужен лучу видимости стража
|
||||||
|
* (check_can_guard_see_kid, seg003:688): тот идёт по ряду между Кидом и
|
||||||
|
* стражем, а оба по условию в одной (= отрисованной) комнате. Колонки за
|
||||||
|
* пределами 0..9 get_tile разрешает через швы — здесь не используются. */
|
||||||
|
uint8_t pop_tile_at(int8_t col, int8_t row) __banked
|
||||||
|
{
|
||||||
|
return get_tile(col, row);
|
||||||
|
}
|
||||||
|
|
||||||
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
|
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
|
||||||
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
|
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
|
||||||
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
|
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
|
||||||
@@ -321,6 +370,11 @@ static void determine_col(void)
|
|||||||
Kid.curr_col = get_tile_div_mod_m7(dx_weight());
|
Kid.curr_col = get_tile_div_mod_m7(dx_weight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* determine_col наружу — для pop_load_fram_det_col (pop_kid): порт связки
|
||||||
|
* load_frame + determine_col (seg006:0144), которую оригинал зовёт перед
|
||||||
|
* control(). См. разбор там. */
|
||||||
|
void pop_determine_col(void) __banked { determine_col(); }
|
||||||
|
|
||||||
/* расстояние до края тайла (для in_wall). */
|
/* расстояние до края тайла (для in_wall). */
|
||||||
static int distance_to_edge(int xpos)
|
static int distance_to_edge(int xpos)
|
||||||
{
|
{
|
||||||
@@ -434,7 +488,22 @@ static void land(void)
|
|||||||
* приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt,
|
* приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt,
|
||||||
* seg003:0AFC). */
|
* seg003:0AFC). */
|
||||||
if (Kid.fall_y < 22) {
|
if (Kid.fall_y < 22) {
|
||||||
|
/* seg005:176 — ВЕТКА С МЕЧОМ, и она не косметика. seq_17_soft_land
|
||||||
|
* заканчивается ВЕЧНЫМ циклом на кадре 109 (`jmp softland_crouch` в
|
||||||
|
* seqtbl), из которого выводит только control_crouched(). А
|
||||||
|
* control() при `Char.sword == sword_2_drawn` до него не доходит:
|
||||||
|
* управление уходит в control_with_sword, который знает лишь кадр
|
||||||
|
* 171 (стойка с мечом). То есть Кид, упавший на этаж С ВЫНУТЫМ
|
||||||
|
* МЕЧОМ, в этот присед садится НАВСЕГДА — ровно то, что поймали на
|
||||||
|
* приёмке уровня 2 (BUG-LAND-SWORD-1). Оригинал такой позы не
|
||||||
|
* допускает: с мечом приземление даёт боевую стойку. */
|
||||||
|
if (Kid.sword == SWORD_2_DRAWN) {
|
||||||
|
seq = SEQ_63_ACTIVE_AFTER_FALL;
|
||||||
|
} else {
|
||||||
seq = SEQ_17_SOFT_LAND;
|
seq = SEQ_17_SOFT_LAND;
|
||||||
|
}
|
||||||
|
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
|
||||||
|
is_guard_notice = 1;
|
||||||
} else {
|
} else {
|
||||||
uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */
|
uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */
|
||||||
pop_kid_hurt = 1;
|
pop_kid_hurt = 1;
|
||||||
@@ -447,6 +516,7 @@ static void land(void)
|
|||||||
pop_kid_dead = 1;
|
pop_kid_dead = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
is_guard_notice = 1; /* seg005:195 */
|
||||||
seq = SEQ_20_MEDIUM_LAND;
|
seq = SEQ_20_MEDIUM_LAND;
|
||||||
}
|
}
|
||||||
Kid.fall_x = Kid.fall_y = 0;
|
Kid.fall_x = Kid.fall_y = 0;
|
||||||
@@ -458,11 +528,34 @@ static void land(void)
|
|||||||
static void start_fall(void)
|
static void start_fall(void)
|
||||||
{
|
{
|
||||||
uint8_t frame = Kid.frame, seq, tile;
|
uint8_t frame = Kid.frame, seq, tile;
|
||||||
|
/* seg006:1044 первым делом убирает меч в ножны: дальше падением рулит
|
||||||
|
* не боевой диспетчер, а обычный. Без этого Kid летит «с клинком», и
|
||||||
|
* control_with_sword разбирает кадры падения как боевые. */
|
||||||
|
Kid.sword = SWORD_0_SHEATHED;
|
||||||
inc_curr_row();
|
inc_curr_row();
|
||||||
|
/* start_chompers() — чомперов ещё нет (L3-CHOMP). */
|
||||||
/* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */
|
/* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */
|
||||||
if (frame == 13) seq = SEQ_19_FALL;
|
if (frame == 13) seq = SEQ_19_FALL;
|
||||||
else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP;
|
else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP;
|
||||||
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
|
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
|
||||||
|
else if (frame >= 81 && frame < 86) {
|
||||||
|
/* сорвался, приземляясь после прыжка вверх: сдвиг ВПЕРЁД на 5 */
|
||||||
|
seq = SEQ_19_FALL;
|
||||||
|
Kid.x = (uint8_t)char_dx_forward(5);
|
||||||
|
pop_load_fram_det_col();
|
||||||
|
}
|
||||||
|
else if (frame >= 150 && frame < 180) {
|
||||||
|
/* Кадры С МЕЧОМ (150..179) — отступил в провал. У оригинала это
|
||||||
|
* ОТДЕЛЬНАЯ последовательность seq_81 (fightfall): падение строго
|
||||||
|
* вниз (set_fall(0,15)), а не seq_7 с дрейфом fall_x=1 — из-за
|
||||||
|
* дрейфа наш Kid уезжал на ~тайл в сторону за время падения.
|
||||||
|
* Плюс сдвиг на 5 назад, если стоит у самой кромки лицом влево.
|
||||||
|
* Ветка стража (seq_82/83) не нужна: start_fall — только Kid.
|
||||||
|
* TODO: droppedout=1 (guard_follows_kid_down, guards.c:296). */
|
||||||
|
if (Kid.direction < 0 && distance_to_edge_weight() <= 7)
|
||||||
|
Kid.x = (uint8_t)char_dx_forward(-5);
|
||||||
|
seq = SEQ_81_FIGHTFALL;
|
||||||
|
}
|
||||||
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
|
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
|
||||||
kid_set_seq(seq);
|
kid_set_seq(seq);
|
||||||
pop_kid_play();
|
pop_kid_play();
|
||||||
@@ -537,6 +630,70 @@ static void check_grab(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void make_loose_fall(int pos, uint8_t modifier);
|
||||||
|
|
||||||
|
/* check_grab_run_jump — ЗАЦЕП ПРЯМО В ПРЫЖКЕ, при Shift, зажатом ЗАРАНЕЕ.
|
||||||
|
*
|
||||||
|
* ВАЖНО: в оригинальной игре этого НЕТ. Это улучшение SDLPoP
|
||||||
|
* (`USE_JUMP_GRAB` + `fixes->enable_jump_grab`, seg006:1228), и там оно
|
||||||
|
* выключено, пока не включены fixes-and-enhancements. Ванильное поведение:
|
||||||
|
* зацепиться можно только в НАЧАЛЕ ПАДЕНИЯ (check_grab, кадры 102..105) —
|
||||||
|
* то есть Shift надо успеть нажать уже в полёте. Пользователь ловил ровно
|
||||||
|
* это и просил «чтобы работало и при заранее зажатом Shift», поэтому
|
||||||
|
* улучшение портировано, но за выключателем POP_ENABLE_JUMP_GRAB
|
||||||
|
* (pop_tune.h): игровая сборка сможет вернуться к оригиналу.
|
||||||
|
*
|
||||||
|
* Условия оригинала: действие — прыжок, кадр 22..23 (прыжок с места) или
|
||||||
|
* 39..41 (разбег-прыжок), «вперёд» ОТПУЩЕНО, «вверх» зажато. Дальше — та
|
||||||
|
* же проба, что у check_grab, но персонаж не падает, а сразу ставится на
|
||||||
|
* кромку и играет seq_9.
|
||||||
|
*
|
||||||
|
* Упрощение против SDLPoP: там колонка захвата правится, когда тайл
|
||||||
|
* оказался в ЧУЖОЙ комнате (curr_room != Char.room) — у нас модель
|
||||||
|
* однокомнатная, get_tile сам резолвит колонки за швом в координатах
|
||||||
|
* ТЕКУЩЕЙ комнаты, поэтому правка не нужна. Зато кнопка/плита за швом
|
||||||
|
* (grab_col вне 0..9) не сработают — тайл принадлежит соседу, а
|
||||||
|
* pop_trob_modif/g_fg у нас про свою комнату; такие случаи пропускаем. */
|
||||||
|
static uint8_t check_grab_run_jump(void)
|
||||||
|
{
|
||||||
|
uint8_t frame = Kid.frame;
|
||||||
|
uint8_t grab_tile;
|
||||||
|
int8_t grab_col, grab_row;
|
||||||
|
|
||||||
|
/* Сам выключатель проверяют ТОЧКИ ВЫЗОВА (иначе SDCC ругается на
|
||||||
|
* недостижимый код при POP_ENABLE_JUMP_GRAB = 1). */
|
||||||
|
if (Kid.action != ACT_RUNJUMP) return 0;
|
||||||
|
if (!((frame >= 22 && frame <= 23) || (frame >= 39 && frame <= 41))) return 0;
|
||||||
|
if (control_x != POP_CONTROL_RELEASED || control_y != POP_CONTROL_HELD_UP) return 0;
|
||||||
|
if (!can_grab_front_above()) return 0;
|
||||||
|
|
||||||
|
grab_tile = g_curr_tile; /* тайл, за который взялись */
|
||||||
|
grab_col = g_infrontx;
|
||||||
|
grab_row = (int8_t)(Kid.curr_row - 1); /* СНЯТЬ до play_seq: он двигает ряд */
|
||||||
|
|
||||||
|
Kid.x = (uint8_t)(pop_x_bump[grab_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
|
||||||
|
Kid.x = (uint8_t)char_dx_forward(Kid.direction < 0 ? -12 : 2);
|
||||||
|
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
|
||||||
|
kid_set_seq(SEQ_9_GRAB_WHILE_JUMPING);
|
||||||
|
pop_kid_play();
|
||||||
|
determine_col();
|
||||||
|
grab_timer = 12;
|
||||||
|
|
||||||
|
/* Хвост оригинала: за что схватились — то и сработает. Нужен потому,
|
||||||
|
* что check_press на следующем кадре уже не поможет: кадр сменится на
|
||||||
|
* 81 (hangdrop), если Shift отпустят сразу. */
|
||||||
|
if (grab_col >= 0 && grab_col <= 9 && grab_row >= 0 && grab_row <= 2) {
|
||||||
|
uint8_t tp = (uint8_t)(grab_row * 10 + grab_col);
|
||||||
|
if (grab_tile == TILE_OPENER || grab_tile == TILE_CLOSER)
|
||||||
|
pop_trigger_button(g_room, tp, grab_tile, pop_trob_modif(g_room)[tp]);
|
||||||
|
else if (grab_tile == TILE_LOOSE) {
|
||||||
|
is_guard_notice = 1;
|
||||||
|
make_loose_fall(tp, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static void do_fall(void)
|
static void do_fall(void)
|
||||||
{
|
{
|
||||||
uint8_t nrow = (uint8_t)(Kid.curr_row + 1);
|
uint8_t nrow = (uint8_t)(Kid.curr_row + 1);
|
||||||
@@ -696,6 +853,47 @@ uint8_t pop_jump_up_seq(void) __banked
|
|||||||
return jump_up_plain();
|
return jump_up_plain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* run_jump (seg005:0AA8), часть «выровнять по кромке» ------------------ *
|
||||||
|
* Оригинал НЕ отталкивается откуда попало: перед разбег-прыжком он смотрит
|
||||||
|
* на 1–2 тайла вперёд и, если там провал (или пика), подгоняет X так, чтобы
|
||||||
|
* толчок пришёлся ровно на кромку пола. Иначе прыжок стартует в случайной
|
||||||
|
* фазе бегового цикла — а запаса у seq_4 почти нет: суммарный dx кадров
|
||||||
|
* 34..44 равен 62 px при ширине тайла 14, то есть ровно 4 колонки и меньше
|
||||||
|
* половины тайла сверху. Поэтому без выравнивания провал в ТРИ пустых
|
||||||
|
* тайла (перелёт с колонки 5 на колонку 1) становится непроходимым —
|
||||||
|
* BUG-RJUMP-1, уровень 2, комнаты 1 и 9.
|
||||||
|
*
|
||||||
|
* Возврат: 1 — прыгать (Kid.x уже подогнан), 0 — прыжок ОТМЕНИТЬ. Отмена
|
||||||
|
* в оригинале НЕ гасит control_up: Кид бежит дальше с зажатой «вверх», и на
|
||||||
|
* следующем кадре попытка повторяется — так игрок ловит нужную фазу, просто
|
||||||
|
* удерживая клавишу. */
|
||||||
|
uint8_t pop_run_jump_align(void) __banked
|
||||||
|
{
|
||||||
|
int xpos = char_dx_forward(4);
|
||||||
|
int8_t col = get_tile_div_mod_m7(xpos);
|
||||||
|
uint8_t tf;
|
||||||
|
|
||||||
|
for (tf = 0; tf < 2; tf++) {
|
||||||
|
uint8_t t;
|
||||||
|
col = (int8_t)(col + dir_front[Kid.direction + 1]);
|
||||||
|
t = get_tile(col, Kid.curr_row);
|
||||||
|
if (t != TILE_SPIKE && tile_is_floor(t)) continue; /* тут пол — дальше */
|
||||||
|
|
||||||
|
{
|
||||||
|
int adj = distance_to_edge(xpos) + TILE_SIZEX * (int)tf - TILE_SIZEX;
|
||||||
|
/* Оригинал сравнивает БЕЗЗНАКОВО:
|
||||||
|
* if ((word)adj < (word)-8 || adj >= 2) { if (adj < 128) return; adj = -3; }
|
||||||
|
* что означает ровно «adj НЕ попал в [-8,-1] → прыжка нет».
|
||||||
|
* Ветка с adj = -3 недостижима: distance_to_edge ∈ [0,13] и
|
||||||
|
* tf ∈ {0,1} дают adj ∈ [-14,13], а туда нужно adj >= 128. */
|
||||||
|
if (adj < -8 || adj > -1) return 0;
|
||||||
|
Kid.x = (uint8_t)char_dx_forward((int8_t)(adj + 4));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
|
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
|
||||||
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
|
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
|
||||||
int pop_wall_ahead(void) __banked
|
int pop_wall_ahead(void) __banked
|
||||||
@@ -761,6 +959,58 @@ uint8_t pop_hang_fall_seq(void) __banked
|
|||||||
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
|
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
|
||||||
* приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки
|
* приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки
|
||||||
* пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */
|
* пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */
|
||||||
|
/* Ветка «войти в дверь уровня» из up_pressed (seg005:0482) вместе с
|
||||||
|
* go_up_leveldoor (seg005:0574). Дверь рядом и открыта → поставить Кида в
|
||||||
|
* проём, вернуть 1 (последовательность seq_70 ставит вызывающий — это его
|
||||||
|
* слой); иначе 0 и Кид не тронут.
|
||||||
|
*
|
||||||
|
* Геометрию персонажа меняем в Kid, а НЕ в Char: окно Char вокруг
|
||||||
|
* диспетчера возвращает назад ТОЛЬКО curr_seq и sword
|
||||||
|
* (pop_savekid_state), поэтому во всём порте позицию правит pop_map — как
|
||||||
|
* в pop_down_action и pop_jump_up_seq. Запись в Char.x/Char.direction
|
||||||
|
* отсюда молча теряется (проверено: Кид уходил из проёма влево).
|
||||||
|
*
|
||||||
|
* Порядок проб как в оригинале и с той же семантикой `else if`: берём
|
||||||
|
* ПЕРВУЮ клетку, в которой стоит дверь (под персонажем → за спиной → перед
|
||||||
|
* лицом), и уже её проверяем на открытость — а не ищем «первую открытую».
|
||||||
|
*
|
||||||
|
* Открытость: `modifier >= 42`, то есть створка доехала до верха. Это
|
||||||
|
* ветка fix_exit_door, а не ванильный глобал leveldoor_open: с ванильным
|
||||||
|
* условием в дверь можно войти, пока она ещё ползёт вверх, и персонаж
|
||||||
|
* проходит сквозь опущенную створку.
|
||||||
|
*
|
||||||
|
* Стартовая комната исключена (`level.start_room != drawn_room`): в первой
|
||||||
|
* комнате уровня дверь — та, через которую вошли, и обратно в неё нельзя. */
|
||||||
|
uint8_t pop_leveldoor_enter(void) __banked
|
||||||
|
{
|
||||||
|
int8_t cols[3];
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
if (pop_level_start_room() == g_room) return 0;
|
||||||
|
|
||||||
|
cols[0] = Kid.curr_col;
|
||||||
|
cols[1] = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col);
|
||||||
|
cols[2] = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
if (get_tile(cols[i], Kid.curr_row) != TILE_LEVELDOOR_LEFT)
|
||||||
|
continue;
|
||||||
|
/* дверь найдена — дальше решает только её modif (см. выше) */
|
||||||
|
if (cols[i] < 0 || cols[i] > 9) return 0; /* дверь в шве: не наша */
|
||||||
|
if (pop_trob_modif(g_room)[Kid.curr_row * 10 + cols[i]] < 42)
|
||||||
|
return 0;
|
||||||
|
/* go_up_leveldoor (seg005:0574): встать РОВНО в проём и развернуться
|
||||||
|
* ВЛЕВО. Направление тут не косметика: все DX последовательности
|
||||||
|
* seq_70 ОТРИЦАТЕЛЬНЫЕ (−5,−4,−4,−2,−3,−1,−3,−1,−2), а
|
||||||
|
* char_dx_forward при взгляде влево меняет им знак — то есть именно
|
||||||
|
* dir_FF_left и гонит персонажа ВПРАВО-ВВЕРХ по лестнице. При
|
||||||
|
* взгляде вправо тот же seq уводит его влево, ИЗ проёма. */
|
||||||
|
Kid.x = (uint8_t)(pop_x_bump[cols[i] + FIRST_ONSCREEN_COLUMN] + 10);
|
||||||
|
Kid.direction = -1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t pop_down_action(void) __banked
|
uint8_t pop_down_action(void) __banked
|
||||||
{
|
{
|
||||||
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
|
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
|
||||||
@@ -793,6 +1043,9 @@ uint8_t pop_down_action(void) __banked
|
|||||||
uint8_t pop_have_sword;
|
uint8_t pop_have_sword;
|
||||||
uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */
|
uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */
|
||||||
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
|
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
|
||||||
|
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
|
||||||
|
* flash_color; нам хватает двух значений, которые реально встречаются. */
|
||||||
|
uint8_t pop_flash_red;
|
||||||
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
|
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
|
||||||
|
|
||||||
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
|
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
|
||||||
@@ -801,6 +1054,13 @@ static void do_pickup(int8_t obj_type, uint8_t tilepos)
|
|||||||
{
|
{
|
||||||
pickup_obj_type = obj_type;
|
pickup_obj_type = obj_type;
|
||||||
g_fg[tilepos] = TILE_FLOOR;
|
g_fg[tilepos] = TILE_FLOOR;
|
||||||
|
/* curr_room_modif[curr_tilepos] = 0 (seg006:1676): у зелья в модификаторе
|
||||||
|
* лежал тип+фаза пузырька, у меча — счётчик блеска. Тайл стал полом —
|
||||||
|
* мусор в модификаторе не нужен (в исходное его вернёт только рестарт). */
|
||||||
|
{
|
||||||
|
uint8_t *m = pop_trob_modif(g_room);
|
||||||
|
if (m) m[tilepos] = 0;
|
||||||
|
}
|
||||||
pop_item_taken = (uint8_t)(tilepos + 1);
|
pop_item_taken = (uint8_t)(tilepos + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +1076,34 @@ void pop_proc_get_object(void) __banked
|
|||||||
* чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */
|
* чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */
|
||||||
pop_flash_time = 8;
|
pop_flash_time = 8;
|
||||||
}
|
}
|
||||||
pickup_obj_type = 0; /* зелья (типы 1..6) не портированы */
|
else {
|
||||||
|
/* proc_get_object (seg006:1927), ветка зелий. Типы 3/4/6
|
||||||
|
* (перо/переворот/открыть все ворота) — свойства поздних уровней,
|
||||||
|
* их портируем вместе с этими уровнями. */
|
||||||
|
switch (pickup_obj_type) {
|
||||||
|
case 1: /* здоровье: +1 HP, красная вспышка */
|
||||||
|
if (hitp_curr != hitp_max) {
|
||||||
|
hitp_delta = 1;
|
||||||
|
pop_flash_red = 1;
|
||||||
|
pop_flash_time = 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
|
||||||
|
pop_kid_add_life();
|
||||||
|
pop_flash_red = 1;
|
||||||
|
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
|
||||||
|
break;
|
||||||
|
case 5: /* «злое» зелье: −1 HP */
|
||||||
|
hitp_delta = -1;
|
||||||
|
pop_flash_red = 1;
|
||||||
|
pop_flash_time = 2;
|
||||||
|
break;
|
||||||
|
default: /* 3 перо / 4 переворот / 6 открыть —
|
||||||
|
* TODO вместе с их уровнями (7/9) */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pickup_obj_type = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет
|
/* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет
|
||||||
@@ -845,9 +1132,17 @@ uint8_t pop_get_item_action(void) __banked
|
|||||||
Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
|
Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (t != TILE_SWORD) return 0; /* питьё зелий не портировано */
|
{
|
||||||
do_pickup(-1, (uint8_t)(Kid.curr_row * 10 + icol));
|
uint8_t tp = (uint8_t)(Kid.curr_row * 10 + icol);
|
||||||
return 2;
|
if (t == TILE_SWORD) {
|
||||||
|
do_pickup(-1, tp);
|
||||||
|
return 2; /* поднять меч (seq_91) */
|
||||||
|
}
|
||||||
|
/* Зелье: тип лежит в СТАРШИХ битах модификатора (get_item,
|
||||||
|
* seg005: do_pickup(curr_room_modif[curr_tilepos] >> 3)). */
|
||||||
|
do_pickup((int8_t)(pop_trob_modif(g_room)[tp] >> 3), tp);
|
||||||
|
return 3; /* пить (seq_78) */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Диспетчер ----------------------------------------------------- */
|
/* ---- Диспетчер ----------------------------------------------------- */
|
||||||
@@ -864,8 +1159,25 @@ static void check_on_floor(void)
|
|||||||
static void check_action(void)
|
static void check_action(void)
|
||||||
{
|
{
|
||||||
uint8_t action = Kid.action, frame = Kid.frame;
|
uint8_t action = Kid.action, frame = Kid.frame;
|
||||||
|
/* Зацеп прямо в прыжке (улучшение SDLPoP, seg006:0914) — ДО всего
|
||||||
|
* остального: если получилось, кадр уже сменён на вис. */
|
||||||
|
if (POP_ENABLE_JUMP_GRAB && action == ACT_RUNJUMP &&
|
||||||
|
pop_ctrl_shift_held() && check_grab_run_jump())
|
||||||
|
return;
|
||||||
if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) {
|
if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) {
|
||||||
if (frame == 109 || (frame >= 110 && frame <= 119))
|
/* ТОЛЬКО кадр 109 (присед) — как в ванильном seg006:909. Кадры
|
||||||
|
* вставания 110..119 сюда добавляет ОПЦИОНАЛЬНЫЙ FIX_STAND_ON_THIN_AIR,
|
||||||
|
* и он состоит из ТРЁХ частей: (1) эта ветка, (2) отмена падения,
|
||||||
|
* если в 2 px позади ещё пол (seg006:1079), (3) ПРАВКА seqtbl —
|
||||||
|
* dx(1)→dx(0) и dx(−4)→dx(−3) в standup. У нас seqtbl извлечён из
|
||||||
|
* данных оригинала, третью часть не применить, а без неё вставание
|
||||||
|
* уносит весовую точку на 4 px вперёд — в стену. Именно половинчатый
|
||||||
|
* порт этого фикса ронял Кида сквозь пол: комната 5, приземление с
|
||||||
|
* уроном на щебень (2,7) у стены (2,8), вставание → колонка 8 = стена
|
||||||
|
* → start_fall → выпадение в комнату 6 (BUG-STANDUP-1). Ванильное
|
||||||
|
* поведение (кадр 109) от этого свободно; цена — «встать на воздухе»
|
||||||
|
* у самой кромки, ровно как в оригинале 1989 года. */
|
||||||
|
if (frame == 109)
|
||||||
check_on_floor();
|
check_on_floor();
|
||||||
} else if (action == ACT_FREEFALL) {
|
} else if (action == ACT_FREEFALL) {
|
||||||
do_fall();
|
do_fall();
|
||||||
@@ -885,9 +1197,13 @@ static void bumped_fall(void)
|
|||||||
if (Kid.action == ACT_FREEFALL) {
|
if (Kid.action == ACT_FREEFALL) {
|
||||||
Kid.fall_x = 0;
|
Kid.fall_x = 0;
|
||||||
} else {
|
} else {
|
||||||
kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */
|
kid_set_seq(SEQ_45_BUMPFALL);
|
||||||
pop_kid_play();
|
pop_kid_play();
|
||||||
}
|
}
|
||||||
|
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
|
||||||
|
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
|
||||||
|
* взводить обязаны — см. BUG-GUARD-DEAF-1. */
|
||||||
|
is_guard_notice = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ.
|
/* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ.
|
||||||
@@ -911,7 +1227,23 @@ static void bumped_floor(void)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Kid.fall_y = 0;
|
Kid.fall_y = 0;
|
||||||
if (Kid.action == ACT_BUMPED) return; /* уже в отскоке — не рестартить */
|
/* `if (Char.alive)` (seg004:0520) — у ЖИВОГО (alive = −1) сиквенс удара
|
||||||
|
* ставится ВСЕГДА, даже если персонаж уже в отскоке. Раньше здесь стоял
|
||||||
|
* наш guard `if (Kid.action == ACT_BUMPED) return;` («не рестартить») — и
|
||||||
|
* он был корнем BUG-STANDUP-1:
|
||||||
|
* 1) вставание из приседа (medland) двигает Кида на 1 px в стену —
|
||||||
|
* срабатывает бамп;
|
||||||
|
* 2) bumped_floor ПРИЖИМАЕТ y к полу (181), отменяя dy(−2), который
|
||||||
|
* medland сделал в начале;
|
||||||
|
* 3) наш guard возвращает управление, medland продолжается и делает
|
||||||
|
* свои dy(+1), dy(+1) — теперь уже ОТ уровня пола, то есть Кид
|
||||||
|
* оказывается НИЖЕ пола (y = 182);
|
||||||
|
* 4) следующий бамп видит «персонаж ниже пола» (беззнаковая разность
|
||||||
|
* заворачивается) и уводит его в bumped_fall → seq_45 → выпадение
|
||||||
|
* в комнату снизу.
|
||||||
|
* У оригинала шага (3) нет: seq_47 обрывает medland, и лишних dy не
|
||||||
|
* случается. */
|
||||||
|
if (!Kid.alive) return; /* труп — только выравнивание */
|
||||||
frame = Kid.frame;
|
frame = Kid.frame;
|
||||||
/* Кадры прыжков и начала падения — «жёсткий» удар с приседанием
|
/* Кадры прыжков и начала падения — «жёсткий» удар с приседанием
|
||||||
* (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */
|
* (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */
|
||||||
@@ -919,108 +1251,253 @@ static void bumped_floor(void)
|
|||||||
(frame >= 102 && frame < 107))
|
(frame >= 102 && frame < 107))
|
||||||
kid_set_seq(SEQ_46_HARDBUMP);
|
kid_set_seq(SEQ_46_HARDBUMP);
|
||||||
else
|
else
|
||||||
kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */
|
kid_set_seq(SEQ_47_BUMP);
|
||||||
pop_kid_play();
|
pop_kid_play();
|
||||||
|
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять
|
/* ---- check_collisions (seg004:0004) -------------------------------- *
|
||||||
* к грани и отдать исход в bumped_floor/bumped_fall (порт bumped, seg004).
|
* ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка
|
||||||
* Это даёт «шаг назад со звуком» при беге/шаге в стену вместо падения. Уже
|
* удара выбирается обходом 9→0 по переходу флага 0→1.
|
||||||
* в отскоке (action bumped) — только выравниваем, без рестарта сиквенса.
|
*
|
||||||
* В падении/висе не трогаем. */
|
* ПОЧЕМУ НЕ «колонка переднего края» (как было до 2026-08-03, BUG-COLL-1):
|
||||||
|
* колонка края попадает на стену лишь пока край внутри её габаритного окна,
|
||||||
|
* а окно узкое. Для решётки в col9 (x_bump[14]=184, coll_left=191, левая
|
||||||
|
* грань 191+10=201) это ровно 201..204 — четыре пикселя. Беговой кадр
|
||||||
|
* сдвигает край больше чем на четыре, край перескакивает с ≤200 сразу на
|
||||||
|
* ≥205 (это уже колонка 10, соседняя комната, не стена) — бампа нет, а
|
||||||
|
* check_leave тут же видит char_x_right ≥ 201 и уводит Кида СКВОЗЬ
|
||||||
|
* ЗАКРЫТУЮ РЕШЁТКУ. Флаг колонки 9 в оригинале взводится при любом
|
||||||
|
* перелёте, потому что сравнивается не колонка края, а КООРДИНАТЫ:
|
||||||
|
* 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены»)
|
||||||
|
* 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань»)
|
||||||
|
* Тем же корнем лечится влёт в стену на длинном прыжке (комн. 6) и провал
|
||||||
|
* сквозь пол после приземления у стены (комн. 5).
|
||||||
|
*
|
||||||
|
* ТРИ РЯДА — как в оригинале (было: один, и это оказалось BUG-JUMPWALL-1).
|
||||||
|
* Оригинал каждый кадр считает флаги для рядов row−1, row, row+1, а
|
||||||
|
* move_coll_to_prev (seg004:00DF) в начале СЛЕДУЮЩЕГО кадра выбирает из них
|
||||||
|
* тот, что соответствует ряду прошлого кадра. Так «прошлые» флаги остаются
|
||||||
|
* настоящими и на кадре смены ряда.
|
||||||
|
*
|
||||||
|
* Наш прежний упрощённый вариант (prev = 3 = «уже перекрывал») подавлял бамп
|
||||||
|
* ровно на этом кадре — а в падении ряд меняется чуть ли не каждый кадр, и
|
||||||
|
* переход 0→1 на стене приходится как раз на него. Симптом: недолетевший
|
||||||
|
* прыжок уходит СКВОЗЬ кладку по свободной траектории (ур. 3 комн. 14).
|
||||||
|
* Цена трёх рядов — 3×14 get_tile на кадр вместо 14; замерено, что это
|
||||||
|
* дешевле любого из обходных вариантов, а поведение совпадает с оригиналом.
|
||||||
|
*
|
||||||
|
* Ряды вне комнаты (−1 и 3) считаются штатно: get_tile сам резолвит ряд −1
|
||||||
|
* через комнату сверху (g_above), а ряд 3 отдаёт WALL — то же «через край
|
||||||
|
* не бампим», что и раньше. */
|
||||||
|
/* Колонки считаем от −2 до 11, а не 0..9: колонки за швом — это тайлы
|
||||||
|
* СОСЕДНЕЙ комнаты (get_tile резолвит их через g_lcol/g_rcol), и решётка
|
||||||
|
* соседа обязана бампить так же, как своя (иначе Kid уходит сквозь
|
||||||
|
* закрытые ворота шва — старый BUG-SEAM-PINGPONG). Оригинал делает то же
|
||||||
|
* самое: get_row_collision_data идёт от col(char_x_left)−1 до
|
||||||
|
* col(char_x_right)+2 и резолвит комнату через find_room_of_tile, а
|
||||||
|
* координата грани (coll_tile_left_xpos) остаётся в системе ОТРИСОВАННОЙ
|
||||||
|
* комнаты — как у нас pop_x_bump[col + FIRST_ONSCREEN_COLUMN]. */
|
||||||
|
#define COLL_C0 (-2) /* первая считаемая колонка */
|
||||||
|
#define COLL_N 14 /* колонки −2..11 */
|
||||||
|
#define COLL_IDX(c) ((c) - COLL_C0)
|
||||||
|
#define COLL_ROW_NONE ((int8_t)-128)
|
||||||
|
static uint8_t coll_prev[COLL_N], coll_curr[COLL_N];
|
||||||
|
static uint8_t coll_above[COLL_N], coll_below[COLL_N];
|
||||||
|
static int8_t coll_prev_row = COLL_ROW_NONE; /* ряд, к которому относится coll_prev */
|
||||||
|
/* «нет колонки»: НЕ −1, потому что −1/−2 — законные колонки шва. */
|
||||||
|
#define BUMP_NONE ((int8_t)-128)
|
||||||
|
static int8_t bump_col_l; /* bump_col_left_of_wall (Kid лицом ВПРАВО) */
|
||||||
|
static int8_t bump_col_r; /* bump_col_right_of_wall (Kid лицом ВЛЕВО) */
|
||||||
|
static int coll_xl, coll_xr; /* char_x_left_coll / char_x_right_coll */
|
||||||
|
|
||||||
|
/* Смена комнаты: колонки перепроецированы, прошлые флаги недействительны.
|
||||||
|
* В оригинале это несовпадение prev_coll_room и curr_row_coll_room —
|
||||||
|
* на кадре входа бампа нет. */
|
||||||
|
static void pop_coll_invalidate(void) { coll_prev_row = COLL_ROW_NONE; }
|
||||||
|
|
||||||
|
/* set_char_collision (seg006:0723), горизонтальная часть: габарит персонажа
|
||||||
|
* в комнатных координатах. char_x_left = obj_x/2 + 58 (минус полуширина при
|
||||||
|
* взгляде вправо) — это ровно наш char_x_forward_edge для переднего края. */
|
||||||
|
static void set_char_collision(void)
|
||||||
|
{
|
||||||
|
int e = char_x_forward_edge();
|
||||||
|
int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */
|
||||||
|
if (Kid.direction < 0) { coll_xl = e; coll_xr = e + wh; }
|
||||||
|
else { coll_xr = e; coll_xl = e - wh; }
|
||||||
|
if (kid_cur_flags() & FRAME_THIN) { coll_xl += 4; coll_xr -= 4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* get_row_collision_data (seg004:0185) для одного ряда: бит0 = «персонаж
|
||||||
|
* достал левую грань стены колонки», бит1 = «правую». Оригинал считает
|
||||||
|
* колонки от col(char_x_left)−1 до col(char_x_right)+2 и оставляет прочие
|
||||||
|
* как есть; мы считаем все четырнадцать — они дешёвые (индекс в массив +
|
||||||
|
* switch), зато не остаётся протухших ячеек (FIX_COLL_FLAGS SDLPoP). */
|
||||||
|
/* move_coll_to_prev (seg004:00DF): в prev кладём флаги ряда, в котором
|
||||||
|
* персонаж был НА ПРОШЛОМ кадре. Массивы curr/above/below в этот момент
|
||||||
|
* ещё хранят прошлый кадр, поэтому нужный ряд просто выбирается из них по
|
||||||
|
* разнице рядов. Условия — дословно оригинальные, включая ±3: ряд может
|
||||||
|
* «завернуться» при смене комнаты (2 -> 0 вниз, 0 -> 2 вверх), и тогда
|
||||||
|
* геометрия колонок та же, что у текущего ряда.
|
||||||
|
*
|
||||||
|
* Первый кадр (и кадр после смены комнаты, pop_coll_invalidate) прошлого
|
||||||
|
* ряда не имеет: prev = «уже перекрывал всё», бампа на нём нет. */
|
||||||
|
static void move_coll_to_prev(int8_t row)
|
||||||
|
{
|
||||||
|
const uint8_t *src;
|
||||||
|
int8_t i;
|
||||||
|
if (coll_prev_row == COLL_ROW_NONE) {
|
||||||
|
for (i = 0; i < COLL_N; i++) coll_prev[i] = 3;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (row == coll_prev_row || row + 3 == coll_prev_row || row - 3 == coll_prev_row)
|
||||||
|
src = coll_curr; /* ряд не менялся */
|
||||||
|
else if (row + 1 == coll_prev_row || row - 2 == coll_prev_row)
|
||||||
|
src = coll_above; /* поднялся на ряд */
|
||||||
|
else
|
||||||
|
src = coll_below; /* опустился на ряд */
|
||||||
|
for (i = 0; i < COLL_N; i++) coll_prev[i] = src[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void get_row_collision_data(int8_t row, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int8_t col;
|
||||||
|
for (col = COLL_C0; col < COLL_C0 + COLL_N; col++) {
|
||||||
|
uint8_t f = 0;
|
||||||
|
uint8_t wt = wall_type(get_tile(col, row));
|
||||||
|
if (wt) {
|
||||||
|
int coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||||
|
if (wall_dl[wt] + coll_left < coll_xr) f |= 1;
|
||||||
|
if (coll_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2;
|
||||||
|
}
|
||||||
|
flags[COLL_IDX(col)] = f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void check_collisions(void)
|
||||||
|
{
|
||||||
|
int8_t col;
|
||||||
|
bump_col_l = bump_col_r = BUMP_NONE;
|
||||||
|
/* seg004:0044 — на кадрах разворота коллизии не считаются вовсе. */
|
||||||
|
if (Kid.action == ACT_TURN) return;
|
||||||
|
/* Вне рядов комнаты (падение мимо пола) get_tile отдаёт WALL во всех
|
||||||
|
* колонках — считать по нему нечего. Стену в падении держит
|
||||||
|
* do_fall/in_wall, выход из комнаты — check_leave_below. */
|
||||||
|
if (Kid.curr_row < 0 || Kid.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; }
|
||||||
|
|
||||||
|
set_char_collision();
|
||||||
|
move_coll_to_prev(Kid.curr_row);
|
||||||
|
coll_prev_row = Kid.curr_row;
|
||||||
|
/* Порядок важен: prev уже забран, теперь три ряда пересчитываются
|
||||||
|
* НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). */
|
||||||
|
get_row_collision_data(Kid.curr_row, coll_curr);
|
||||||
|
get_row_collision_data((int8_t)(Kid.curr_row + 1), coll_below);
|
||||||
|
get_row_collision_data((int8_t)(Kid.curr_row - 1), coll_above);
|
||||||
|
/* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
|
||||||
|
* в которой флаг перешёл 0→1. */
|
||||||
|
for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --col) {
|
||||||
|
uint8_t p = coll_prev[COLL_IDX(col)], c = coll_curr[COLL_IDX(col)];
|
||||||
|
if (!(p & 1) && (c & 1)) bump_col_l = col;
|
||||||
|
if (!(p & 2) && (c & 2)) bump_col_r = col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* is_obstacle (seg004:037E): препятствие ли тайл колонки на самом деле.
|
||||||
|
* Побочно оставляет тайл в g_curr_tile (как curr_tile2 у оригинала). */
|
||||||
|
static uint8_t is_obstacle_at_col(int8_t col)
|
||||||
|
{
|
||||||
|
uint8_t t = get_tile(col, Kid.curr_row);
|
||||||
|
if (t == TILE_POTION) return 0; /* зелье не бампит */
|
||||||
|
if (t == TILE_GATE) return (uint8_t)!gate_passable(col, Kid.curr_row);
|
||||||
|
if (t == TILE_CHOMPER) /* закрыт только при modif==2 */
|
||||||
|
return (uint8_t)(col >= 0 && col <= 9 &&
|
||||||
|
pop_trob_modif(g_room)[Kid.curr_row * 10 + col] == 2);
|
||||||
|
/* ветка «прыжок сквозь зеркало» (seg004:239) — уровень 4, не портируем */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bumped (seg004:0448). delta_x — сдвиг персонажа В КООРДИНАТАХ (не
|
||||||
|
* «вперёд»), push_dir — куда толкает: <0 влево, >=0 вправо. Чем кончится
|
||||||
|
* удар, решает тайл, на котором персонаж оказался после отжатия: сквозь
|
||||||
|
* стену/верх двери он стоит на СОСЕДНЕЙ клетке (в сторону толчка), для
|
||||||
|
* прочих препятствий (ворота, зеркало, кусаки) — на клетке препятствия. */
|
||||||
|
static void bumped(int delta_x, int8_t push_dir, int8_t tcol)
|
||||||
|
{
|
||||||
|
uint8_t ct;
|
||||||
|
if (Kid.alive >= 0) return; /* труп не бампит */
|
||||||
|
Kid.x = (uint8_t)((int)Kid.x + delta_x);
|
||||||
|
determine_col();
|
||||||
|
ct = g_curr_tile; /* тайл препятствия */
|
||||||
|
if (push_dir < 0) { /* толчок ВЛЕВО */
|
||||||
|
if (ct == TILE_WALL)
|
||||||
|
ct = get_tile((int8_t)(tcol - 1), Kid.curr_row);
|
||||||
|
} else { /* толчок ВПРАВО */
|
||||||
|
if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL)
|
||||||
|
ct = get_tile((int8_t)(tcol + 1), Kid.curr_row);
|
||||||
|
}
|
||||||
|
if (tile_is_floor(ct)) bumped_floor();
|
||||||
|
else bumped_fall();
|
||||||
|
determine_col();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check_bumped_look_right (seg004:030A): Kid смотрит ВПРАВО и достал ЛЕВУЮ
|
||||||
|
* грань стены — отжать влево. */
|
||||||
|
static void check_bumped_look_right(void)
|
||||||
|
{
|
||||||
|
int8_t col = bump_col_l;
|
||||||
|
uint8_t wt;
|
||||||
|
int coll_left;
|
||||||
|
if (Kid.direction != 0) return; /* (меча в руке у нас нет) */
|
||||||
|
if (!is_obstacle_at_col(col)) return;
|
||||||
|
/* Прежде чем удариться о стену — попытка зацепиться за её верх
|
||||||
|
* (seg004:0205, то же улучшение). Не вышло — тайл препятствия надо
|
||||||
|
* перечитать: проба зацепа сбила g_curr_tile. */
|
||||||
|
if (POP_ENABLE_JUMP_GRAB && pop_ctrl_shift_held()) {
|
||||||
|
if (check_grab_run_jump()) return;
|
||||||
|
if (!is_obstacle_at_col(col)) return;
|
||||||
|
}
|
||||||
|
wt = wall_type(g_curr_tile);
|
||||||
|
if (!wt) return;
|
||||||
|
coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||||
|
bumped(wall_dl[wt] + coll_left - coll_xr, -1, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check_bumped_look_left (seg004:02D2): Kid смотрит ВЛЕВО и достал ПРАВУЮ
|
||||||
|
* грань стены — отжать вправо. */
|
||||||
|
static void check_bumped_look_left(void)
|
||||||
|
{
|
||||||
|
int8_t col = bump_col_r;
|
||||||
|
uint8_t wt;
|
||||||
|
int coll_left;
|
||||||
|
if (Kid.direction >= 0) return;
|
||||||
|
if (!is_obstacle_at_col(col)) return;
|
||||||
|
if (POP_ENABLE_JUMP_GRAB && pop_ctrl_shift_held()) { /* seg004:0186 */
|
||||||
|
if (check_grab_run_jump()) return;
|
||||||
|
if (!is_obstacle_at_col(col)) return;
|
||||||
|
}
|
||||||
|
wt = wall_type(g_curr_tile);
|
||||||
|
if (!wt) return;
|
||||||
|
coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||||
|
bumped(coll_left - wall_dr[wt] + TILE_RIGHTX - coll_xl, 0, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check_bumped (seg004:029D). */
|
||||||
static void check_bumped(void)
|
static void check_bumped(void)
|
||||||
{
|
{
|
||||||
uint8_t t, wt; int8_t tcol; int d;
|
/* Гарды РОВНО как в оригинале: вис и подтягивание. ПАДЕНИЕ И ПОЛЁТ НЕ
|
||||||
/* Не бампим в падении/висе/подтягивании (seg004 check_bumped guard'ы:
|
* ИСКЛЮЧЕНЫ — bumped_fall (seg004:04E4) специально разбирает случай
|
||||||
* action hang_climb/hang_straight + кадры подъёма 135..148 — иначе
|
* actions_4_in_freefall (гасит fall_x и отжимает на 4). Наш прежний
|
||||||
* climb-up у стены прерывается отскоком). FREEFALL — наш guard: стену
|
* guard `action == FREEFALL || MIDAIR -> return` и был вторым корнем
|
||||||
* в падении держит do_fall/in_wall. */
|
* BUG-COLL-1: длинный прыжок в стену (комната 6, между (0,6) и (0,7))
|
||||||
if (Kid.action == ACT_FREEFALL || Kid.action == ACT_HANGCLIMB ||
|
* ничем не останавливался, и Кид оказывался ВНУТРИ кладки. */
|
||||||
Kid.action == ACT_HANGSTRAIGHT || Kid.action == ACT_MIDAIR) return;
|
if (Kid.action == ACT_HANGCLIMB || Kid.action == ACT_HANGSTRAIGHT) return;
|
||||||
if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */
|
if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */
|
||||||
/* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb),
|
/* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb),
|
||||||
* т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку
|
* т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку
|
||||||
* hang->hang_fall->seq_11 (правильное приземление). */
|
* hang->hang_fall->seq_11 (правильное приземление). */
|
||||||
if (Kid.frame >= 87 && Kid.frame < 100) return;
|
if (Kid.frame >= 87 && Kid.frame < 100) return;
|
||||||
/* Вне рядов комнаты (падение мимо пола): get_tile даёт WALL, а
|
if (bump_col_l != BUMP_NONE) check_bumped_look_right();
|
||||||
* dist_from_wall_forward/pop_x_bump[] — мусор с большим отриц. сдвигом →
|
else if (bump_col_r != BUMP_NONE) check_bumped_look_left();
|
||||||
* underflow X и уход в стену. fell_out ловит do_fall. */
|
|
||||||
if (Kid.curr_row < 0 || Kid.curr_row > 2) return;
|
|
||||||
/* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются
|
|
||||||
* (bump_col_* сбрасываются и функция выходит) — bump'а в развороте нет. */
|
|
||||||
if (Kid.action == ACT_TURN) return;
|
|
||||||
/* Стена ищется по колонке, в которую упирается ПЕРЕДНИЙ КРАЙ персонажа
|
|
||||||
* (порт seg004: bump_col_left/right_of_wall — колонка, где край пересёк
|
|
||||||
* грань; НЕ curr_col!). Через curr_col было ДВА расхождения: (1) весовая
|
|
||||||
* точка кадра заезжает за грань раньше края, curr_col становился −1 на
|
|
||||||
* шве, ворота оказывались «под персонажем» и гард односторонней стены их
|
|
||||||
* снимал — Kid уходил в соседнюю комнату СКВОЗЬ ОПУЩЕННУЮ РЕШЁТКУ;
|
|
||||||
* (2) тот же гард был нужен только из-за отсутствия проверки разворота
|
|
||||||
* (см. выше). Колонка переднего края даёт и то, и другое даром: стоя
|
|
||||||
* СПРАВА от ворот лицом вправо, край в соседней колонке — ворота не
|
|
||||||
* бампят; заехав краем за их грань лицом влево — бампят. */
|
|
||||||
tcol = get_tile_div_mod_m7(char_front_coll());
|
|
||||||
t = get_tile(tcol, Kid.curr_row);
|
|
||||||
wt = wall_type(t);
|
|
||||||
/* EDGE-триггер (порт check_collisions + get_row_collision_data, seg004:0004).
|
|
||||||
* Оригинал считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ направления:
|
|
||||||
* 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены»)
|
|
||||||
* 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань»)
|
|
||||||
* и зовёт bumped() ТОЛЬКО когда нужный флаг перешёл 0 -> 1 (взгляд вправо
|
|
||||||
* смотрит на 0x0F, влево — на 0xF0). Без этого проверка «уровнем» ловит
|
|
||||||
* ЛЮБОЕ перекрытие: Kid, стоящий В проёме опущенной решётки, при развороте
|
|
||||||
* на месте получал ложный бамп и выпихивался сквозь неё в соседнюю
|
|
||||||
* комнату. С флагами от габарита разворот их не меняет — бампа нет. */
|
|
||||||
{
|
|
||||||
uint8_t f = 0, prev, need;
|
|
||||||
if (wt) {
|
|
||||||
int coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
|
||||||
int lw = wall_dl[wt] + coll_left; /* get_left_wall_xpos */
|
|
||||||
int rw = coll_left - wall_dr[wt] + TILE_RIGHTX; /* get_right_wall_xpos */
|
|
||||||
int e = char_x_forward_edge(), wh = ((int)kid_fp_width() + 1) >> 1;
|
|
||||||
int xl, xr;
|
|
||||||
if (Kid.direction < 0) { xl = e; xr = e + wh; } /* габарит персонажа */
|
|
||||||
else { xr = e; xl = e - wh; }
|
|
||||||
if (kid_cur_flags() & FRAME_THIN) { xl += 4; xr -= 4; }
|
|
||||||
if (lw < xr) f |= 1;
|
|
||||||
if (rw > xl) f |= 2;
|
|
||||||
}
|
|
||||||
/* прошлый кадр: та же колонка — её флаги; другая — считаем, что не
|
|
||||||
* перекрывали; «нет данных» (смена комнаты) — считаем, что перекрывали
|
|
||||||
* всё, чтобы на кадре входа бампа не было (в оригинале это даёт
|
|
||||||
* несовпадение prev_coll_room и curr_row_coll_room). */
|
|
||||||
prev = (bump_prev_col == BUMP_COL_NONE) ? 3u
|
|
||||||
: (bump_prev_col == tcol ? bump_prev_flags : 0u);
|
|
||||||
bump_prev_col = tcol;
|
|
||||||
bump_prev_flags = f;
|
|
||||||
if (!wt) return;
|
|
||||||
need = (Kid.direction < 0) ? 2u : 1u;
|
|
||||||
if (!(f & need) || (prev & need)) return; /* не перекрыл / уже перекрывал */
|
|
||||||
}
|
|
||||||
if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row))
|
|
||||||
return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */
|
|
||||||
d = dist_from_wall_forward(t, tcol);
|
|
||||||
if (d >= 0) return; /* ещё не зашёл за грань */
|
|
||||||
Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */
|
|
||||||
determine_col();
|
|
||||||
/* Хвост bumped (seg004:04E4): чем КОНЧИТСЯ удар, решает тайл, на котором
|
|
||||||
* персонаж оказался после отжатия от грани. Сквозь стену/верх двери он
|
|
||||||
* стоит на СОСЕДНЕЙ клетке (в сторону толчка), для прочих препятствий
|
|
||||||
* (ворота, зеркало, кусаки) — на клетке самого препятствия (у ворот пол
|
|
||||||
* есть, поэтому tile_is_floor их считает полом). */
|
|
||||||
{
|
|
||||||
uint8_t ct = t;
|
|
||||||
if (Kid.direction < 0) { /* смотрит влево => толчок ВПРАВО */
|
|
||||||
if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL)
|
|
||||||
ct = get_tile((int8_t)(tcol + 1), Kid.curr_row);
|
|
||||||
} else { /* смотрит вправо => толчок ВЛЕВО */
|
|
||||||
if (ct == TILE_WALL)
|
|
||||||
ct = get_tile((int8_t)(tcol - 1), Kid.curr_row);
|
|
||||||
}
|
|
||||||
if (tile_is_floor(ct)) bumped_floor();
|
|
||||||
else bumped_fall();
|
|
||||||
}
|
|
||||||
determine_col();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Loose floors (seg007) ---------------------------------------- *
|
/* ---- Loose floors (seg007) ---------------------------------------- *
|
||||||
@@ -1066,10 +1543,13 @@ static void check_press(void)
|
|||||||
int8_t c = Kid.curr_col;
|
int8_t c = Kid.curr_col;
|
||||||
if (g_above && g_link_u && c >= 0 && c <= 9 &&
|
if (g_above && g_link_u && c >= 0 && c <= 9 &&
|
||||||
(g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) &&
|
(g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) &&
|
||||||
(int8_t)pop_ceil_modif[c] <= 0)
|
(int8_t)pop_ceil_modif[c] <= 0) {
|
||||||
pop_ceil_modif[c] = 1; /* make_loose_fall(1) */
|
pop_ceil_modif[c] = 1; /* make_loose_fall(1) */
|
||||||
|
is_guard_notice = 1; /* seg006:1734 */
|
||||||
|
}
|
||||||
} else if (get_tile_above_char() == TILE_LOOSE) {
|
} else if (get_tile_above_char() == TILE_LOOSE) {
|
||||||
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
|
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
|
||||||
|
is_guard_notice = 1; /* seg006:1734 */
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1083,8 +1563,10 @@ static void check_press(void)
|
|||||||
uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
|
uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
|
||||||
if (t == TILE_OPENER || t == TILE_CLOSER)
|
if (t == TILE_OPENER || t == TILE_CLOSER)
|
||||||
pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]);
|
pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]);
|
||||||
else if (t == TILE_LOOSE)
|
else if (t == TILE_LOOSE) {
|
||||||
make_loose_fall(tp, 1);
|
make_loose_fall(tp, 1);
|
||||||
|
is_guard_notice = 1; /* seg006:1734 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1124,7 +1606,8 @@ void pop_check_knock(void) __banked
|
|||||||
|
|
||||||
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
|
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
|
||||||
* индексируются позицией тайла и не должны утечь из старой комнаты в новую.
|
* индексируются позицией тайла и не должны утечь из старой комнаты в новую.
|
||||||
* Плюс останавливает падающий кусок (pop_bg). */
|
* Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и
|
||||||
|
* должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */
|
||||||
void pop_loose_reset(void) __banked
|
void pop_loose_reset(void) __banked
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
@@ -1137,7 +1620,7 @@ void pop_loose_reset(void) __banked
|
|||||||
pop_ceil_fell = 0;
|
pop_ceil_fell = 0;
|
||||||
pop_debris_at = 0;
|
pop_debris_at = 0;
|
||||||
pop_loose_landed = 0;
|
pop_loose_landed = 0;
|
||||||
pop_loose_mob_reset();
|
pop_loose_mob_room_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
|
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
|
||||||
@@ -1281,6 +1764,21 @@ void pop_loose_tick(void) __banked
|
|||||||
* check_spike_below колонки берутся get_tile_div_mod БЕЗ −7.
|
* check_spike_below колонки берутся get_tile_div_mod БЕЗ −7.
|
||||||
*
|
*
|
||||||
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
|
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
|
||||||
|
/* Вторая половина ветки двери уровня из clip_char (seg006:1231):
|
||||||
|
* obj_clip_right = leveldoor_right — ЭКРАННЫЙ X, правее которого спрайт не
|
||||||
|
* рисуется. 0 = обрезки нет.
|
||||||
|
*
|
||||||
|
* Почему обрезка, а не fore-слой: створка и косяк уходят в оригинале
|
||||||
|
* ЦЕЛИКОМ в backtable (draw_leveldoor, все add_backtable), то есть рисуются
|
||||||
|
* ПОД персонажем и перекрыть его не могут. Единственный способ спрятать
|
||||||
|
* поднимающегося по лестнице — срезать сам спрайт. */
|
||||||
|
int pop_clip_char_right(void) __banked
|
||||||
|
{
|
||||||
|
if (Kid.frame >= 224 && Kid.frame < 229)
|
||||||
|
return pop_leveldoor_right;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
|
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
|
||||||
{
|
{
|
||||||
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
|
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
|
||||||
@@ -1290,6 +1788,14 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
|
|||||||
int8_t cL, cR, trow, clip_row;
|
int8_t cL, cR, trow, clip_row;
|
||||||
uint8_t t;
|
uint8_t t;
|
||||||
|
|
||||||
|
/* Кадры подъёма по лестнице двери уровня (clip_char, seg006:1231):
|
||||||
|
* персонаж режется по низу поднятой створки. ВНИМАНИЕ: комментарий в
|
||||||
|
* SDLPoP говорит «frames 217..228», а КОД проверяет
|
||||||
|
* `>= frame_224_exit_stairs_8`, то есть 224..228 — портируем по коду.
|
||||||
|
* Правую кромку отдаёт pop_clip_char_right (парная половина ветки). */
|
||||||
|
if (Kid.frame >= 224 && Kid.frame < 229)
|
||||||
|
return pop_leveldoor_ybottom + 1;
|
||||||
|
|
||||||
if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
|
if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
|
||||||
xr = xl + wh; /* char_x_right */
|
xr = xl + wh; /* char_x_right */
|
||||||
top_y = obj_y - (int)h + 1; /* char_top_y */
|
top_y = obj_y - (int)h + 1; /* char_top_y */
|
||||||
@@ -1489,6 +1995,7 @@ void pop_phys_tick(void) __banked
|
|||||||
fall_accel();
|
fall_accel();
|
||||||
fall_speed();
|
fall_speed();
|
||||||
determine_col();
|
determine_col();
|
||||||
|
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
|
||||||
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
|
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
|
||||||
check_action();
|
check_action();
|
||||||
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
|
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
|
||||||
|
|||||||
@@ -30,13 +30,33 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
|
|||||||
* Звать в enter_room. */
|
* Звать в enter_room. */
|
||||||
void pop_map_set_room(uint8_t room) __banked;
|
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;
|
||||||
|
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
|
||||||
|
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
|
||||||
void pop_kid_hp_reset(void) __banked;
|
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 или
|
||||||
* tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл
|
* tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл
|
||||||
@@ -46,7 +66,9 @@ 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) __banked; /* 0 нет / 1 присесть / 2 поднять меч */
|
/* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
|
||||||
|
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) */
|
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 для
|
||||||
@@ -113,6 +135,10 @@ void pop_phys_tick(void) __banked;
|
|||||||
* Возврат: КОМНАТНЫЙ 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) __banked;
|
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 (бежать / осторожный шаг / стоп у стены). */
|
||||||
#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */
|
#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */
|
||||||
@@ -128,6 +154,13 @@ uint8_t pop_edge_type(void) __banked;
|
|||||||
* control_running (чистый стоп у стены). */
|
* control_running (чистый стоп у стены). */
|
||||||
int pop_wall_ahead(void) __banked;
|
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. */
|
||||||
@@ -142,4 +175,10 @@ int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у сте
|
|||||||
uint8_t pop_hang_fall_seq(void) __banked; /* 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) __banked; /* 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
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ void pop_redraw_needed(void)
|
|||||||
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(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_FLOOR: pop_floor_bake(row, col); break;
|
||||||
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(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;
|
default: break;
|
||||||
}
|
}
|
||||||
if (--rd_cnt[i]) left++;
|
if (--rd_cnt[i]) left++;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
|
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
|
||||||
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
|
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
|
||||||
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
|
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
|
||||||
|
#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */
|
||||||
|
|
||||||
/* Виды перерисовки полосы у потолка (по колонке). */
|
/* Виды перерисовки полосы у потолка (по колонке). */
|
||||||
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
|
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
|
||||||
|
|||||||
@@ -11,5 +11,50 @@
|
|||||||
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
|
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
|
||||||
*/
|
*/
|
||||||
#include "pop_state.h"
|
#include "pop_state.h"
|
||||||
|
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
|
||||||
|
|
||||||
uint8_t pop_loose_landed;
|
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++; }
|
||||||
|
|||||||
@@ -14,4 +14,25 @@
|
|||||||
* разбирает логика loose-полов (pop_map, W1/W2). */
|
* разбирает логика loose-полов (pop_map, W1/W2). */
|
||||||
extern uint8_t pop_loose_landed;
|
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
|
#endif
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
|
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
|
||||||
* нельзя звать из __banked-кода. */
|
* нельзя звать из __banked-кода. */
|
||||||
#include "pop_bg.h"
|
#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
|
||||||
@@ -21,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
|
||||||
@@ -47,12 +50,12 @@ static uint8_t trob_drawn[POP_ROOMTILES];
|
|||||||
|
|
||||||
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
|
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
|
||||||
* выбора следующего кадра пламени. */
|
* выбора следующего кадра пламени. */
|
||||||
static unsigned long trob_seed;
|
static pop_rnd_t trob_seed;
|
||||||
|
|
||||||
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
|
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
|
||||||
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -61,18 +64,37 @@ void pop_trob_reset(void)
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -87,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; }
|
||||||
@@ -98,7 +120,7 @@ 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++) trob_drawn[i] = 0;
|
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||||
@@ -109,7 +131,7 @@ void pop_trob_room_changed(void)
|
|||||||
* тайлов: факелам — случайная стартовая фаза пламени (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;
|
||||||
@@ -259,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,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++)
|
||||||
@@ -362,7 +391,7 @@ static uint8_t torch_tick;
|
|||||||
#define TORCH_ANIM_NOW 1
|
#define TORCH_ANIM_NOW 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void pop_process_trobs(uint8_t cur_room)
|
void pop_process_trobs(uint8_t cur_room) __banked
|
||||||
{
|
{
|
||||||
uint8_t i, w = 0;
|
uint8_t i, w = 0;
|
||||||
#if TORCH_ANIM_DIV > 1
|
#if TORCH_ANIM_DIV > 1
|
||||||
@@ -460,6 +489,15 @@ void pop_process_trobs(uint8_t cur_room)
|
|||||||
* страницы: живой trob метит каждый кадр по одной. */
|
* страницы: живой trob метит каждый кадр по одной. */
|
||||||
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
|
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
|
||||||
}
|
}
|
||||||
|
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) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
|
||||||
|
|||||||
@@ -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 */
|
||||||
@@ -16,8 +16,10 @@
|
|||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "pop_trob.h"
|
#include "pop_trob.h"
|
||||||
#include "pop_redraw.h"
|
#include "pop_redraw.h"
|
||||||
|
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
|
||||||
#include "pop_guard.h"
|
#include "pop_guard.h"
|
||||||
#include "pop_cheat.h"
|
#include "pop_cheat.h"
|
||||||
|
#include "pop_tune.h"
|
||||||
|
|
||||||
__sfr __at (0xFE) io_border;
|
__sfr __at (0xFE) io_border;
|
||||||
|
|
||||||
@@ -46,23 +48,58 @@ __sfr __at (0xFE) io_border;
|
|||||||
#define PROF(n) ((void)0)
|
#define PROF(n) ((void)0)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Сколько банков грузит crt0_banked (читает его стартовый код). */
|
/* Сколько банков грузит crt0_banked (читает его стартовый код).
|
||||||
const uint8_t n_banks = 3;
|
* 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. */
|
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
||||||
uint8_t pop_cheats;
|
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 /* тиков до приёма ↑ (труп успел лечь) */
|
||||||
|
|
||||||
@@ -73,6 +110,7 @@ uint8_t pop_cheats;
|
|||||||
* Убирается закомментированием ОДНОЙ строки ниже. */
|
* Убирается закомментированием ОДНОЙ строки ниже. */
|
||||||
#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 переключал комнату без длинного списка параметров. */
|
||||||
@@ -80,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) */
|
||||||
@@ -98,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 стабилен
|
||||||
@@ -111,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 — вспышка фона активна (нулевая запись палитры подменена). */
|
||||||
@@ -137,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),
|
||||||
@@ -181,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); /* плита-потолок: пробой снизу + провал */
|
||||||
@@ -191,10 +288,16 @@ static void enter_room(uint8_t room)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pop_map_set_room(room);
|
pop_map_set_room(room);
|
||||||
|
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
|
||||||
|
* (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
|
||||||
|
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
|
||||||
|
* стража никогда не срабатывает. */
|
||||||
|
Kid.room = room;
|
||||||
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
|
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_guard_enter(room); /* страж комнаты (порт enter_guard) */
|
||||||
|
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
|
||||||
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
||||||
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
||||||
* а не по свежему backtable. Фаза P0. */
|
* а не по свежему backtable. Фаза P0. */
|
||||||
@@ -222,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
|
||||||
@@ -232,7 +448,8 @@ int main(void)
|
|||||||
#ifdef ROOMNAV
|
#ifdef ROOMNAV
|
||||||
uint8_t nav_prev = 0;
|
uint8_t nav_prev = 0;
|
||||||
#endif
|
#endif
|
||||||
uint8_t cheat_prev = 0;
|
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");
|
||||||
@@ -250,11 +467,13 @@ int main(void)
|
|||||||
puts("kid atlas load failed");
|
puts("kid atlas load failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (pop_level_load("LEVELS\\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);
|
||||||
@@ -268,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(),
|
||||||
@@ -287,6 +502,7 @@ 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, чтоб одно нажатие = один
|
||||||
@@ -309,11 +525,47 @@ 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 — убить
|
{ /* Читы (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 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 && 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;
|
cheat_prev = k;
|
||||||
|
immo_prev = i;
|
||||||
|
sword_prev = s;
|
||||||
|
nlvl_prev = nl;
|
||||||
|
xdec_prev = xd;
|
||||||
|
xinc_prev = xi;
|
||||||
}
|
}
|
||||||
if (frozen) {
|
if (frozen) {
|
||||||
PROF(0); /* чёрный: ждём vsync */
|
PROF(0); /* чёрный: ждём vsync */
|
||||||
@@ -324,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));
|
||||||
@@ -333,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): всё, кроме пустоты/стены/верхов колонны и двери. */
|
||||||
@@ -356,6 +616,7 @@ 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_guard_heal(); /* и прошлый кадр стража */
|
||||||
@@ -364,22 +625,42 @@ int main(void)
|
|||||||
kid_tick(); /* play_seq: следующий кадр */
|
kid_tick(); /* play_seq: следующий кадр */
|
||||||
pop_phys_tick(); /* K3: падение/приземление/стена */
|
pop_phys_tick(); /* K3: падение/приземление/стена */
|
||||||
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
|
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);
|
||||||
@@ -410,16 +691,35 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
|
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
|
||||||
@@ -427,26 +727,26 @@ int main(void)
|
|||||||
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;
|
||||||
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
|
pop_level_set_tile(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;
|
||||||
}
|
}
|
||||||
@@ -463,8 +763,12 @@ 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 */
|
||||||
@@ -472,15 +776,26 @@ int main(void)
|
|||||||
}
|
}
|
||||||
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
|
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
|
||||||
PROF(6); /* спрайты: страж, Kid, fore поверх */
|
PROF(6); /* спрайты: страж, Kid, fore поверх */
|
||||||
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */
|
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
|
||||||
|
* персонажа (см. guard_over_kid). */
|
||||||
|
uint8_t g_after = guard_over_kid();
|
||||||
|
if (!g_after) pop_guard_draw();
|
||||||
PROF(6); /* kid_draw (тот же циан) */
|
PROF(6); /* kid_draw (тот же циан) */
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
pop_hp_draw(); /* полоса HP — только при изменении */
|
||||||
PROF(6); /* fore поверх Kid + борта */
|
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);
|
||||||
@@ -531,6 +846,7 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
|
||||||
pop_ctrl_close();
|
pop_ctrl_close();
|
||||||
closegraph();
|
closegraph();
|
||||||
pop_bg_free();
|
pop_bg_free();
|
||||||
|
|||||||
@@ -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,7 @@ 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__)),
|
OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
"..", "poc", "res", "kid")
|
"..", "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__)),
|
||||||
@@ -44,6 +44,18 @@ def parse_frame_table(txt, name="frame_table_kid"):
|
|||||||
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))]
|
||||||
@@ -69,6 +81,7 @@ def main():
|
|||||||
gframes = parse_frame_table(seg006, "frame_tbl_guard")
|
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"
|
||||||
@@ -102,7 +115,22 @@ def main():
|
|||||||
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 подряд
|
# kid_data.bin: кадры (по 5 байт) + seqtbl подряд
|
||||||
os.makedirs(OUT_BIN_DIR, exist_ok=True)
|
os.makedirs(OUT_BIN_DIR, exist_ok=True)
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ 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
|
||||||
GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
|
GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
|
||||||
SWORD_IDS = tuple(range(20, 28)) # sword_tbl для кадров 229..236 (найти-убрать)
|
# 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
|
||||||
|
|
||||||
@@ -150,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)
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
|
|
||||||
## Ближайшее
|
## Ближайшее
|
||||||
|
|
||||||
|
- [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план:
|
||||||
|
`docs/host-tests-plan.md`. Обвязка уже готова и обкатана
|
||||||
|
(`testkit/`, первый потребитель — PoP/roomtest). Не срочно, но
|
||||||
|
обязательно: это основной продукт репозитория, а покрыт он сейчас
|
||||||
|
только интеграционными тестами в MAME. Начинать с `time/` и
|
||||||
|
`stdio/` (нулевые швы), затем геометрия `libbgi/common`, затем
|
||||||
|
фейковые ESTEX/BIOS в тестовом crt0 — они открывают `file/`, `io/`,
|
||||||
|
`conio/` и проверку гардов (`_fd_guard`).
|
||||||
|
|
||||||
Порядок реализации связки: **сначала цепочка irq, потом FPS-делитель
|
Порядок реализации связки: **сначала цепочка irq, потом FPS-делитель
|
||||||
поверх неё** (делитель — просто один слот цепи; так снимается EBUSY
|
поверх неё** (делитель — просто один слот цепи; так снимается EBUSY
|
||||||
на сосуществование со своим тиком приложения).
|
на сосуществование со своим тиком приложения).
|
||||||
@@ -52,6 +61,35 @@
|
|||||||
|
|
||||||
## Auto-banking (memory/banking_roadmap.md)
|
## Auto-banking (memory/banking_roadmap.md)
|
||||||
|
|
||||||
|
- [ ] **Сверять `n_banks` с реальным числом банков — ошибкой сборки.**
|
||||||
|
Сейчас число банков задаётся ДВАЖДЫ и вручную: флагами `--bank N=…`
|
||||||
|
у `sprinter-cc` и константой `const uint8_t n_banks = N;` в
|
||||||
|
приложении (её читает `crt0_banked` при старте, чтобы выделить
|
||||||
|
EMM-страницы и загрузить в них образы банков). Рассогласование не
|
||||||
|
даёт ни ошибки компиляции, ни ошибки линковки — только зависание в
|
||||||
|
рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в
|
||||||
|
окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом
|
||||||
|
уже есть в шапке `runtime/bank.s`).
|
||||||
|
**Найдено 2026-08-05** на PoP/roomtest: добавили пятый банк
|
||||||
|
(`--bank 5=pop_ctrl.c`), забыли `n_banks` — игра доходила до конца
|
||||||
|
загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз
|
||||||
|
занял заметно больше, чем сама правка: симптом (зависание в чужом
|
||||||
|
коде) никак не указывает на причину.
|
||||||
|
Сверять надо не количество, а **максимальный индекс** банка: `--bank`
|
||||||
|
нумеруются с 1 и в принципе могут идти с дырами, а `crt0_banked`
|
||||||
|
заполняет `_bank_pages[1..n_banks]` подряд.
|
||||||
|
Варианты, от простого к правильному:
|
||||||
|
1. **Проверка после линковки** — вытащить значение `_n_banks` из
|
||||||
|
`.ihx`/`.map` (это `const` в `_CODE`, адрес известен из карты) и
|
||||||
|
сравнить с `max(N)` по флагам; не совпало — `sprinter-cc` падает
|
||||||
|
с внятным сообщением. Ничего не ломает в существующих
|
||||||
|
приложениях, ставится сразу.
|
||||||
|
2. **Генерировать самим `sprinter-cc`** — он уже так делает, когда
|
||||||
|
банков нет (`const unsigned char n_banks = 0;`). Правильнее по
|
||||||
|
сути (единственный источник истины), но требует убрать константу
|
||||||
|
из всех банковых приложений, иначе дубль символа: `tests/banked`,
|
||||||
|
`banktest`, `banklocl`, `bankedbg`, `applications/PoP/roomtest`.
|
||||||
|
|
||||||
Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
|
Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
|
||||||
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
|
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
|
||||||
first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,
|
first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,
|
||||||
@@ -279,15 +317,39 @@ Quick wins:
|
|||||||
- [ ] **kbd_raw: частые Rx-overrun в MAME при тапах** (roomtest,
|
- [ ] **kbd_raw: частые Rx-overrun в MAME при тапах** (roomtest,
|
||||||
2026-07-22) — замеры watchpoint-счётчиками: при стабильном
|
2026-07-22) — замеры watchpoint-счётчиками: при стабильном
|
||||||
удержании клавиши overrun'ов ноль, но каждый быстрый тап (5 байт:
|
удержании клавиши overrun'ов ноль, но каждый быстрый тап (5 байт:
|
||||||
E0 74 + E0 F0 74 при FIFO 3) даёт overrun. Подозрение: dev-MAME
|
E0 74 + E0 F0 74 при FIFO 3) даёт overrun.
|
||||||
не эмулирует прерывание SIO на каждый принятый байт (байты
|
**Уточнение 2026-08-01 (по коду драйвера, прежняя гипотеза
|
||||||
вычерпываются только кадровым IRQ 50 Гц) и/или подаёт пачку без
|
НЕВЕРНА):** dev-MAME per-byte INT ДАЁТ —
|
||||||
реальных ~1 мс/байт — на железе per-byte INT должен делать
|
`mame/sources/MAME/src/mame/sinclair/sprinter.cpp`
|
||||||
overrun'ы редкостью. Проверить mame/sources/MAME/src/mame/
|
`on_kbd_data()` ставит `m_irqs->in_set<1>()` на каждый принятый
|
||||||
sinclair/sprinter.cpp (путь байта клавиатуры → INT) и при
|
байт. Но тут же заводит `m_irq_off_timer` на **32 такта CPU**, и
|
||||||
желании поправить dev-MAME. Recovery-политика уже терпима к
|
`irq_off()` снимает линию — то есть импульс, пришедшийся на наше
|
||||||
частым overrun'ам (селективный wipe, docs/kbd-games.md), но
|
DI-окно, теряется НАСОВСЕМ (байт остаётся в FIFO до следующего
|
||||||
аккорд «держу →, тапнул ↑» остаётся уязвим.
|
IRQ или кадрового 50 Гц). А DI-окна у нас длинные: ядра
|
||||||
|
акселератора держат `di` на весь блит (`libbgi/bgi256/
|
||||||
|
_bgi_blit_cols_raw.c`), это сотни микросекунд против 32 тактов.
|
||||||
|
**Замерено 2026-08-01 (PoP roomtest, счётчики в MAME): обе
|
||||||
|
«наши» гипотезы отпали.** (а) `kbd_raw_poll()` из главного цикла
|
||||||
|
не меняет ничего (9/10 с ним и без); (б) снятие `di` в accel-ядрах
|
||||||
|
УРОНИЛО машину — режим «акселератор при EI» тут недоступен; и сама
|
||||||
|
длина DI ни при чём (в замороженном кадре без блитов потерь
|
||||||
|
БОЛЬШЕ). Байт теряется ДО чтения порта: на 49 прочитанных байт
|
||||||
|
только 28 входов в клавиатурную ветку, т.е. ~44 % импульсов
|
||||||
|
запроса не обслужены и 3-байтовый FIFO переполняется. Главный
|
||||||
|
подозреваемый — `m_irq_off_timer` в sprinter.cpp: он ОДИН на два
|
||||||
|
источника (экран + клавиатура), `irq_off()` гасит обе линии, так
|
||||||
|
что кадровое прерывание способно обрезать клавиатурный импульс.
|
||||||
|
**ЗАКРЫТО в рабочем объёме 2026-08-01:** лечится ПЛОТНЫМ опросом —
|
||||||
|
`kbd_raw_poll` повешен idle-хуком графики (`gfx_set_idle_hook`,
|
||||||
|
новый API libbgi) на ожидание кадра, где процессор всё равно
|
||||||
|
простаивает ~2/3 периода. 35 нажатий с зажатым Shift → 35
|
||||||
|
дошедших make против 9 из 10 без хука. Пользователь на ручной
|
||||||
|
проверке отмечает, что редкие пропуски всё же ощущаются — остаток
|
||||||
|
отложен до финальной полировки, следующий шаг описан там же.
|
||||||
|
Полный протокол — applications/PoP/roomtest/TASKS.md, KBD-1.
|
||||||
|
Recovery-политика уже терпима к частым overrun'ам (селективный
|
||||||
|
wipe, docs/kbd-games.md), но аккорды «держу →, тапнул ↑» и
|
||||||
|
«держу Shift, тапаю ←» остаются уязвимы.
|
||||||
|
|
||||||
## Known quirks (зафиксированы, обходы в libc)
|
## Known quirks (зафиксированы, обходы в libc)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# План: модульные тесты libc и libbgi под ucsim_z80
|
||||||
|
|
||||||
|
Статус: **не начато**, задача на будущее. Обвязка уже готова и обкатана —
|
||||||
|
`testkit/` (см. `testkit/README.md`); первый потребитель —
|
||||||
|
`applications/PoP/roomtest/tests-host/`. Этот документ — про то, как накрыть
|
||||||
|
тем же способом основной продукт репозитория.
|
||||||
|
|
||||||
|
## Что это НЕ заменяет
|
||||||
|
|
||||||
|
В `tests/` уже лежат 62 каталога — это **интеграционные** тесты: одна фича =
|
||||||
|
одна программа, которая пакуется на дискету и гоняется в MAME
|
||||||
|
(`docs/mame-autotest.md`). Они проверяют, что API работает на живой машине:
|
||||||
|
ESTEX, BIOS, диск, экран, тайминги.
|
||||||
|
|
||||||
|
Модульные тесты их не отменяют, а дополняют с другой стороны:
|
||||||
|
|
||||||
|
| | `tests/` (MAME) | `tests-host` (ucsim) |
|
||||||
|
|---|---|---|
|
||||||
|
| что проверяет | работает ли на машине | верна ли логика |
|
||||||
|
| граничные случаи | 1–2 на фичу | десятки, дёшево |
|
||||||
|
| время прогона | десятки секунд | миллисекунды |
|
||||||
|
| ловит | железо, тайминги, банки | арифметику, краевые условия, регрессии |
|
||||||
|
|
||||||
|
Правило разделения то же, что уже записано для PoP: **что можно проверить
|
||||||
|
без железа — проверять в ucsim, MAME оставить железу.**
|
||||||
|
|
||||||
|
## Три группы модулей
|
||||||
|
|
||||||
|
### 1. Чистая логика — тестируется как есть, швов не нужно
|
||||||
|
|
||||||
|
Здесь можно начинать в тот же день, когда задачу возьмут в работу.
|
||||||
|
|
||||||
|
**libc:**
|
||||||
|
|
||||||
|
- `time/` — `_tm_is_leap`, `_tm_mdays`, `_tm_month_days`, `_tm_year_days`,
|
||||||
|
`mktime`, `gmtime`, `localtime`, `asctime`, `ctime`. Классическая
|
||||||
|
календарная арифметика: високосные годы, границы месяцев, переходы через
|
||||||
|
год, круговой прогон `mktime(gmtime(t)) == t`. Идеальный первый набор —
|
||||||
|
много краевых случаев и ноль зависимостей.
|
||||||
|
- `stdio/` — `dec_print`, `hex8/16/32`, `_scanf_core`, `sscanf`. Формат и
|
||||||
|
разбор: ширина, знак, переполнение, мусор на входе.
|
||||||
|
- `string/`, `stdlib/` — `strlwr`, `strupr`, `max`, `min`.
|
||||||
|
|
||||||
|
**libbgi/common** (115 модулей, почти всё mode-agnostic):
|
||||||
|
|
||||||
|
- `_bgi_isqrt`, `_bgi_trig` — сверить с эталонной формулой на всём
|
||||||
|
диапазоне, ровно как сделано для LCG в `t_geom` у PoP;
|
||||||
|
- `_bgi_lineseg`, `_bgi_styled_line`, `_bgi_poly_edge`, `_bgi_arc_draw` —
|
||||||
|
геометрия и отсечение: линия целиком вне окна, по диагонали через угол,
|
||||||
|
вырожденная в точку;
|
||||||
|
- `_spr_ysort` — порядок сортировки спрайтов;
|
||||||
|
- `_bgi_hspan`, `_bgi_fill_span` — заливка: краевые span'ы, нулевая ширина.
|
||||||
|
|
||||||
|
### 2. Нужны швы — но швы дешёвые
|
||||||
|
|
||||||
|
**Фейковый ESTEX и BIOS прямо в тестовом crt0.** Оба вызываются через
|
||||||
|
`rst #0x10` и `rst #0x08`, то есть через фиксированные векторы в первых
|
||||||
|
байтах памяти. В тестовом бинаре эти адреса наши: можно положить туда
|
||||||
|
обработчик, который эмулирует крошечную файловую систему в ОЗУ и текстовый
|
||||||
|
экран в буфере. Это открывает:
|
||||||
|
|
||||||
|
- `file/` (32 модуля) и `io/` (21) — `fopen`/`fread`/`fwrite`/`fseek`,
|
||||||
|
буферизация (`docs/file-buffering-design.md`), поведение на EOF и
|
||||||
|
ошибках, `errno`;
|
||||||
|
- `conio/` (38) — вывод в буфер вместо экрана, проверка атрибутов,
|
||||||
|
скроллинга, границ окна.
|
||||||
|
|
||||||
|
Отдельно ценно: **гарды**, которые обязаны быть в обеих сборках. `_fd_guard`
|
||||||
|
(девятый `OPEN` вешает DSS) — это ровно тот случай, где нужен тест, а не
|
||||||
|
вера в комментарий: открыть восемь, убедиться, что девятый вернул ошибку и
|
||||||
|
не дошёл до ESTEX.
|
||||||
|
|
||||||
|
**Кадровый буфер в ОЗУ для libbgi.** Если рисующие ядра умеют писать в
|
||||||
|
обычный буфер, а не только в видеопамять, растеризацию можно проверять
|
||||||
|
снимком: нарисовать фигуру, сравнить с эталонным массивом. Начинать с
|
||||||
|
маленьких (8×8, 16×16) — эталон читаемый прямо в исходнике.
|
||||||
|
|
||||||
|
### 3. Только MAME
|
||||||
|
|
||||||
|
Банки и W-окна, EMM, реальный ESTEX/DSS, клавиатурный трамплин и IM2,
|
||||||
|
CBL-звук (`cbl/`), мышь (`mouse/`), `mem/` (это банки и страницы, а не
|
||||||
|
куча), тайминги и бюджет кадра, ускоритель.
|
||||||
|
|
||||||
|
## Обе сборки: fast и safe
|
||||||
|
|
||||||
|
Библиотеки собираются в двух вариантах (`-D*_NOCHECK` вырезает
|
||||||
|
валидацию параметров). Наборы стоит гонять **против обоих**:
|
||||||
|
|
||||||
|
- в safe — что валидация ловит мусорные аргументы и ставит `errno`;
|
||||||
|
- в fast — что вырезание валидации не поменяло поведение на корректных
|
||||||
|
входах.
|
||||||
|
|
||||||
|
Это дешёвая параметризация Makefile (тот же набор, два `OBJS_*`), и она
|
||||||
|
пресекает целый класс расхождений между вариантами.
|
||||||
|
|
||||||
|
## Фазы
|
||||||
|
|
||||||
|
1. **Календарь и формат.** `time/`, `stdio/`. Нулевые швы, максимальная
|
||||||
|
плотность краевых случаев. Цель — обкатать поток работы на libc.
|
||||||
|
2. **Геометрия libbgi.** `common/`: isqrt, тригонометрия, отсечение линий,
|
||||||
|
рёбра полигонов. Тоже без швов.
|
||||||
|
3. **Фейковые ESTEX/BIOS в crt0.** Открывает `file/`, `io/`, `conio/` и
|
||||||
|
гарды. Самый крупный кусок работы и самая большая отдача.
|
||||||
|
4. **Растр libbgi по снимкам** — если окажется, что ядра можно нацелить на
|
||||||
|
буфер в ОЗУ без правок продукта; иначе отложить.
|
||||||
|
5. **Параметризация fast/safe** — после того, как наборов станет заметно.
|
||||||
|
|
||||||
|
## Критерии
|
||||||
|
|
||||||
|
- Тест не считается написанным, пока не проверен мутацией: сломать
|
||||||
|
проверяемое место, убедиться, что набор краснеет. Пустые тесты хуже
|
||||||
|
отсутствующих.
|
||||||
|
- Каждый закрытый баг libc/libbgi получает регрессионный кейс, если он
|
||||||
|
ловится без железа.
|
||||||
|
- `make host-tests` остаётся быстрым: если суммарно перевалит за несколько
|
||||||
|
секунд, делить на быстрый и полный прогон.
|
||||||
|
|
||||||
|
## Организация
|
||||||
|
|
||||||
|
Наборы кладутся рядом с кодом, обвязка общая:
|
||||||
|
|
||||||
|
```
|
||||||
|
libc/tests-host/ наборы по областям: t_time.c, t_stdio.c, …
|
||||||
|
libbgi/tests-host/ t_geom.c, t_raster.c, …
|
||||||
|
```
|
||||||
|
|
||||||
|
Каждому — `Makefile` на пять строк (`TESTKIT`, `ENGINE_DIR`, `OBJS_*`,
|
||||||
|
`include`), и добавить каталог в `HOST_TEST_DIRS` корневого `Makefile`.
|
||||||
|
Подробности — `testkit/README.md`.
|
||||||
@@ -313,6 +313,7 @@ Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс
|
|||||||
| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit |
|
| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit |
|
||||||
| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 |
|
| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 |
|
||||||
| `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) |
|
| `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) |
|
||||||
|
| `uint8_t kbd_raw_poll(void)` | вычерпать FIFO ОПРОСОМ, не дожидаясь прерывания: 0 — было пусто (~40 тактов), 1 — что-то декодировано. Звать МЕЖДУ фазами кадра, после тяжёлых блитов — не раз в кадр (см. ниже, «почему одного прерывания мало») |
|
||||||
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
|
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
|
||||||
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
|
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
|
||||||
| `KBD_LCTRL/LALT/RCTRL/RALT` | коды модификаторов (R* — расширенные, с KBD_EXT); добавлены 2026-07-22 |
|
| `KBD_LCTRL/LALT/RCTRL/RALT` | коды модификаторов (R* — расширенные, с KBD_EXT); добавлены 2026-07-22 |
|
||||||
@@ -335,6 +336,39 @@ git как урок: при повторных «нет эффекта» на с
|
|||||||
брейкпоинтом/watchpoint'ом на конкретный адрес кода, не полагаться
|
брейкпоинтом/watchpoint'ом на конкретный адрес кода, не полагаться
|
||||||
только на визуальный снимок с произвольным таймингом.
|
только на визуальный снимок с произвольным таймингом.
|
||||||
|
|
||||||
|
**ПОЧЕМУ ОДНОГО ПРЕРЫВАНИЯ МАЛО (`kbd_raw_poll`, 2026-08-01).** Приёмный
|
||||||
|
FIFO SIO — 3 байта, и рассчитывать на «каждый байт разбудит нас» нельзя:
|
||||||
|
запрос прерывания клавиатуры держится единицы микросекунд (в dev-MAME —
|
||||||
|
32 такта CPU, `sprinter.cpp` `on_kbd_data` → `irq_off_timer`), а ядра
|
||||||
|
акселератора держат `DI` на весь блит — сотни микросекунд. Импульс,
|
||||||
|
попавший в такое окно, теряется насовсем; байт лежит в FIFO до следующего
|
||||||
|
прерывания (следующий байт либо кадровое, 50 Гц). Трафик же идёт пачками:
|
||||||
|
тап стрелки = 5 байт, а при УДЕРЖИВАЕМОМ Shift PS/2 обрамляет расширенный
|
||||||
|
код «фиктивным шифтом» (`E0 F0 12` … `E0 12`) — по 5 байт и на нажатие, и
|
||||||
|
на отпускание. Три ячейки такую пачку не держат: потерянный make = «нажатие
|
||||||
|
не сработало», потерянный break = залипание. Отсюда `kbd_raw_poll()` —
|
||||||
|
вычерпывание тем же декодером, но из главного цикла. Тело идёт под `DI`
|
||||||
|
(чтение порта 0x18 деструктивно, а read-modify-write карты гонится с
|
||||||
|
трамплином) и БЕЗУСЛОВНО делает `EI` на выходе — из ISR звать нельзя.
|
||||||
|
|
||||||
|
**Как её звать (замеры 2026-08-01, PoP roomtest — читать до применения!).**
|
||||||
|
Расстановка «несколько вызовов за кадр, после тяжёлых фаз» **не даёт
|
||||||
|
ничего**: 9 дошедших make-байт из 10 нажатий и с ней, и без неё — такие
|
||||||
|
вызовы попадают ровно в участки с разрешёнными прерываниями и лишь
|
||||||
|
дублируют трамплин. Там же измерено, что длина DI-окон графики на потери
|
||||||
|
НЕ влияет (в кадре вообще без блитов потерь больше), а теряется байт ДО
|
||||||
|
чтения порта: примерно 44 % импульсов запроса прерывания не обслуживаются,
|
||||||
|
и трёхбайтовый FIFO переполняется.
|
||||||
|
|
||||||
|
**Работает только ПЛОТНЫЙ опрос — порядка раза в 0.5 мс.** Штатный способ
|
||||||
|
взять эту частоту даром — повесить функцию idle-хуком графики:
|
||||||
|
`gfx_set_idle_hook()` (`<gfx.h>`) зовёт её, пока `gfx_wait_vsync` крутит
|
||||||
|
опрос луча, а это ~2/3 периода кадра. Проверено: 35 нажатий стрелки с
|
||||||
|
зажатым Shift → 35 дошедших make против 9 из 10 без хука. Остаточные
|
||||||
|
редкие потери возможны (пачка целиком внутри DI-окна одного accel-прохода);
|
||||||
|
полный протокол и что делать дальше — `applications/PoP/roomtest/TASKS.md`,
|
||||||
|
задача KBD-1.
|
||||||
|
|
||||||
**ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/
|
**ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/
|
||||||
kbd_mod_state` НЕ получают новых событий (в т.ч. CTRLKEY подряд отдаёт
|
kbd_mod_state` НЕ получают новых событий (в т.ч. CTRLKEY подряд отдаёт
|
||||||
то же самое, что было на момент открытия — резидентный обработчик DSS,
|
то же самое, что было на момент открытия — резидентный обработчик DSS,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ argv 3431
|
|||||||
assrtest 3847
|
assrtest 3847
|
||||||
atlas 9193
|
atlas 9193
|
||||||
attrprob 4087
|
attrprob 4087
|
||||||
balls 6031
|
|
||||||
banked 1056
|
banked 1056
|
||||||
bankedbg 1067
|
bankedbg 1067
|
||||||
banklocl 4683
|
banklocl 4683
|
||||||
@@ -15,9 +14,9 @@ bios_text 4461
|
|||||||
blitperf 5860
|
blitperf 5860
|
||||||
blitw 4939
|
blitw 4939
|
||||||
cat 913
|
cat 913
|
||||||
cblstream 6431
|
cblstream 6464
|
||||||
cbltest 6580
|
cbltest 6613
|
||||||
cblwav 6648
|
cblwav 6681
|
||||||
conio 4605
|
conio 4605
|
||||||
conio2 3929
|
conio2 3929
|
||||||
dec_test 860
|
dec_test 860
|
||||||
@@ -27,30 +26,25 @@ fdmax 6023
|
|||||||
filetest 10547
|
filetest 10547
|
||||||
fpsdiv 4870
|
fpsdiv 4870
|
||||||
gets 509
|
gets 509
|
||||||
gfx_dbuf 5112
|
gfx_dbuf 5134
|
||||||
gfx_demo 4117
|
gfx_demo 4117
|
||||||
gfxbanks 6044
|
gfxbanks 6044
|
||||||
hello 4167
|
hello 4167
|
||||||
hello2 4244
|
hello2 4244
|
||||||
irqtest 6647
|
irqtest 6680
|
||||||
|
kbdpoll 1219
|
||||||
kbdraw 4911
|
kbdraw 4911
|
||||||
ls 4835
|
ls 4835
|
||||||
malloc 4461
|
malloc 4461
|
||||||
mdview 18878
|
|
||||||
mdview2 27031
|
|
||||||
mem_test 4555
|
mem_test 4555
|
||||||
mouse 4382
|
mouse 4382
|
||||||
openenv 6124
|
openenv 6124
|
||||||
palfile 5406
|
palfile 5406
|
||||||
ptime 5744
|
ptime 5744
|
||||||
rpgprof 12082
|
rt_test 5187
|
||||||
rpgwalk 10309
|
|
||||||
rt_test 5154
|
|
||||||
scroll 2974
|
|
||||||
seek 4188
|
seek 4188
|
||||||
simple 955
|
simple 955
|
||||||
solidt 11531
|
solidt 11531
|
||||||
space 11272
|
|
||||||
spranim 9431
|
spranim 9431
|
||||||
spriteclip 4119
|
spriteclip 4119
|
||||||
sprites 6791
|
sprites 6791
|
||||||
|
|||||||
|
@@ -175,11 +175,14 @@ void gfx_scroll_v(uint8_t src_page, uint8_t dst_page,
|
|||||||
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
|
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
|
||||||
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
|
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
|
||||||
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
|
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
|
||||||
2. **ОЗУ-копия per-page или общая.** `C-Compiler/applications/PoP/docs/double_buffer_plan.md`
|
2. **ОЗУ-копия per-page или общая.** Ранний план дабл-буфера PoP исходил из
|
||||||
пишет «теневая копия одна — общая». Если тень физически одна (а не адресуется
|
«теневая копия одна — общая»; **практикой это опровергнуто**: у каждой
|
||||||
по базе `0xC000/0xC140` как VRAM), page0 и page1 не удержат фон **разных**
|
страницы СВОЯ теневая ОЗУ-копия, heal берёт фон из копии той страницы, в
|
||||||
положений камеры во время скролла → пинг-понг (§8) сломается. Проверить:
|
которую рисуем (`applications/PoP/roomtest/CLAUDE.md`, раздел
|
||||||
записать разный фон в page0 и page1 банком `0x50`, сверить чтение обеих.
|
«Дабл-буфер»; `roomtest.c enter_room` рисует фон в обе страницы по
|
||||||
|
отдельности именно поэтому). Значит пинг-понг (§8) в принципе рабочий,
|
||||||
|
но перед опорой на него — подтвердить на своём коде: записать разный фон
|
||||||
|
в page0 и page1 банком `0x50`, сверить чтение обеих.
|
||||||
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
|
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
|
||||||
fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
|
fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,11 @@ void _bgi_poly_edge(int x0, int y0, int x1, int y1);
|
|||||||
extern void _cbl_port_ref(void);
|
extern void _cbl_port_ref(void);
|
||||||
extern void _cbl_port_unref(void);
|
extern void _cbl_port_unref(void);
|
||||||
|
|
||||||
|
/* Idle-хук ожидания кадра (common/_gfx_idle_state.c, ставится
|
||||||
|
* gfx_set_idle_hook). NULL = нет; зовётся из лучевого цикла
|
||||||
|
* gfx_wait_vsync. */
|
||||||
|
extern void (*_gfx_idle_hook)(void);
|
||||||
|
|
||||||
/* ---- FPS-делитель (frame pacing) --------------------------------- *
|
/* ---- FPS-делитель (frame pacing) --------------------------------- *
|
||||||
* Фоновый счётчик кадров через цепочку кадровых IRQ (libc/irq). Данные
|
* Фоновый счётчик кадров через цепочку кадровых IRQ (libc/irq). Данные
|
||||||
* в common/_gfx_fps_state.c; при _gfx_fps_div<=1 путь не активен и
|
* в common/_gfx_fps_state.c; при _gfx_fps_div<=1 путь не активен и
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* _gfx_idle_state — указатель на idle-хук, который gfx_wait_vsync зовёт,
|
||||||
|
* пока ждёт луч. Отдельный data-модуль: на него ссылаются и установщик
|
||||||
|
* (gfx_set_idle_hook), и сам цикл ожидания, а тянуть один из-за другого
|
||||||
|
* в программу, которая хук не ставит, ни к чему. Без инициализации
|
||||||
|
* (crt0 зануляет _DATA — NULL = хука нет).
|
||||||
|
*/
|
||||||
|
#include "../_bgi.h"
|
||||||
|
|
||||||
|
void (*_gfx_idle_hook)(void);
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
* сверху для column-major бесплатна: колонка = непрерывный кусок ОЗУ, старт
|
* сверху для column-major бесплатна: колонка = непрерывный кусок ОЗУ, старт
|
||||||
* просто сдвигается на skip байт, длина accel-блока уменьшается.
|
* просто сдвигается на skip байт, длина accel-блока уменьшается.
|
||||||
*
|
*
|
||||||
* Здесь же живёт полное тело блита колонками — gfx_blit_cols() тонкая
|
* Тело (вместе с клип-логикой) — в gfx_blit_cols_part_w.c: этот вызов =
|
||||||
* обёртка (skip=0, rows=0), чтобы не дублировать клип-логику.
|
* «без ограничения ширины». Подробности зеркала/клипа — см. шапку
|
||||||
* Подробности зеркала/клипа — см. шапку gfx_blit_cols.c.
|
* gfx_blit_cols.c.
|
||||||
*/
|
*/
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "../_bgi.h"
|
#include "../_bgi.h"
|
||||||
@@ -19,44 +19,5 @@
|
|||||||
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
||||||
int skip, int rows)
|
int skip, int rows)
|
||||||
{
|
{
|
||||||
const uint8_t *p = (const uint8_t *)img;
|
gfx_blit_cols_part_w(x, y, img, flip, skip, rows, 0);
|
||||||
int full_w = p[0] | (p[1] << 8);
|
|
||||||
int full_h = p[2] | (p[3] << 8);
|
|
||||||
const uint8_t *px = p + 4;
|
|
||||||
int w = full_w, h;
|
|
||||||
int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */
|
|
||||||
int sy; /* пропуск строк сверху колонки */
|
|
||||||
int cx = 0; /* пропуск колонок слева экрана */
|
|
||||||
const uint8_t *src;
|
|
||||||
uint8_t *dst;
|
|
||||||
|
|
||||||
if (full_w <= 0 || full_h <= 0)
|
|
||||||
return;
|
|
||||||
if (skip < 0) skip = 0;
|
|
||||||
if (skip >= full_h)
|
|
||||||
return; /* обрезано целиком */
|
|
||||||
sy = skip;
|
|
||||||
h = full_h - skip;
|
|
||||||
if (rows > 0 && rows < h)
|
|
||||||
h = rows;
|
|
||||||
|
|
||||||
/* Клип по экрану — только если хоть один край вылезает. */
|
|
||||||
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
|
|
||||||
if (x < 0) { cx = -x; w += x; x = 0; }
|
|
||||||
if (y < 0) { sy -= y; h += y; y = 0; }
|
|
||||||
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
|
|
||||||
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
|
|
||||||
if (w <= 0 || h <= 0) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. */
|
|
||||||
src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px;
|
|
||||||
if (cx)
|
|
||||||
src += cx * sstride;
|
|
||||||
src += sy;
|
|
||||||
|
|
||||||
_bgi_begin();
|
|
||||||
dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x);
|
|
||||||
_bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y);
|
|
||||||
_bgi_end();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* gfx_blit_cols_part_noclip — блит спрайта column-major БЕЗ клипа по экрану,
|
||||||
|
* с обрезкой сверху (как gfx_blit_cols_part). Пара к gfx_blit_cols_part
|
||||||
|
* ровно так же, как gfx_blit_part_noclip — к gfx_blit_part.
|
||||||
|
*
|
||||||
|
* Зачем: у персонажей PoP спрайт почти всегда целиком на экране, а общий
|
||||||
|
* gfx_blit_cols_part платит за проверку четырёх границ, пересчёт w/h/cx/sy и
|
||||||
|
* 16-битную арифметику независимо от того, вылезает край или нет — замер в
|
||||||
|
* MAME (roomtest, страж + клинок): 5 622 такта подготовки против 13 596 на
|
||||||
|
* сам accel-проход. Здесь та же колонка уходит в _bgi_blit_cols_raw без
|
||||||
|
* подготовки.
|
||||||
|
*
|
||||||
|
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
|
||||||
|
* - прямоугольник назначения целиком на экране;
|
||||||
|
* - ширина и высота кадра <= 255, y <= 255;
|
||||||
|
* - skip < высоты кадра.
|
||||||
|
* Не выполняется — зовите gfx_blit_cols_part.
|
||||||
|
*
|
||||||
|
* Флип, как и в gfx_blit_cols_part, бесплатный: подаём src с последней
|
||||||
|
* колонки и отрицательный страйд — accel сам кладёт её в левый x.
|
||||||
|
*
|
||||||
|
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
|
||||||
|
* собранного с --w3, её вызывать нельзя — после _bgi_begin окно W3 занято
|
||||||
|
* видеобанком и код вызывающего исчезает из адресного пространства.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "../_bgi.h"
|
||||||
|
|
||||||
|
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
|
||||||
|
uint8_t skip, uint8_t rows)
|
||||||
|
{
|
||||||
|
const uint8_t *p = (const uint8_t *)img;
|
||||||
|
uint8_t w = p[0]; /* ширина кадра (< 256) */
|
||||||
|
uint8_t fh = p[2]; /* высота кадра (< 256) */
|
||||||
|
const uint8_t *src = p + 4;
|
||||||
|
uint8_t h;
|
||||||
|
|
||||||
|
if (w == 0 || fh == 0 || skip >= fh) return;
|
||||||
|
h = (uint8_t)(fh - skip);
|
||||||
|
if (rows && rows < h) h = rows;
|
||||||
|
|
||||||
|
if (flip) src += (uint16_t)(w - 1) * fh; /* последняя колонка -> левый x */
|
||||||
|
src += skip;
|
||||||
|
|
||||||
|
_bgi_begin();
|
||||||
|
_bgi_blit_cols_raw(src, (uint8_t *)(_gfx_addr_base + (uint16_t)x),
|
||||||
|
w, h, flip ? -(int)fh : (int)fh, (uint8_t)y);
|
||||||
|
_bgi_end();
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* gfx_blit_cols_part_w — блит спрайта column-major с обрезкой СВЕРХУ
|
||||||
|
* (skip/rows) И ОГРАНИЧЕНИЕМ ШИРИНЫ (maxw): рисуются только первые maxw
|
||||||
|
* колонок футпринта, считая от x. maxw = 0 — «вся ширина кадра».
|
||||||
|
*
|
||||||
|
* Здесь живёт ПОЛНОЕ тело блита колонками; gfx_blit_cols_part (maxw=0) и
|
||||||
|
* gfx_blit_cols (ещё и skip=rows=0) — тонкие обёртки, чтобы клип-логика не
|
||||||
|
* дублировалась. Подробности зеркала/клипа — см. шапку gfx_blit_cols.c.
|
||||||
|
*
|
||||||
|
* Зачем ограничение ширины. Обрезка СПРАВА для column-major — это ровно
|
||||||
|
* уменьшение числа колонок, то есть внутри уже есть (так же клипается край
|
||||||
|
* экрана: `w = _bgi_maxx + 1 - x`), наружу просто не было выведено.
|
||||||
|
* Понадобилось для порта clip_char в PoP: персонаж, поднимающийся по
|
||||||
|
* лестнице за дверью уровня, обязан уходить за правый косяк проёма
|
||||||
|
* (obj_clip_right), а дверь рисуется ПОД ним и перекрыть его не может.
|
||||||
|
*
|
||||||
|
* Важно: ограничение работает и при flip. При зеркале источник читается с
|
||||||
|
* последней колонки к первой (sstride < 0), но первые w нарисованных
|
||||||
|
* колонок в любом случае ложатся в ЛЕВУЮ часть футпринта [x, x+w) — то
|
||||||
|
* есть уменьшение w всегда срезает справа, как и требуется.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "../_bgi.h"
|
||||||
|
|
||||||
|
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
|
||||||
|
int skip, int rows, uint8_t maxw)
|
||||||
|
{
|
||||||
|
const uint8_t *p = (const uint8_t *)img;
|
||||||
|
int full_w = p[0] | (p[1] << 8);
|
||||||
|
int full_h = p[2] | (p[3] << 8);
|
||||||
|
const uint8_t *px = p + 4;
|
||||||
|
int w = full_w, h;
|
||||||
|
int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */
|
||||||
|
int sy; /* пропуск строк сверху колонки */
|
||||||
|
int cx = 0; /* пропуск колонок слева экрана */
|
||||||
|
const uint8_t *src;
|
||||||
|
uint8_t *dst;
|
||||||
|
|
||||||
|
if (full_w <= 0 || full_h <= 0)
|
||||||
|
return;
|
||||||
|
if (maxw && (int)maxw < w)
|
||||||
|
w = (int)maxw; /* обрезка справа (см. шапку) */
|
||||||
|
if (skip < 0) skip = 0;
|
||||||
|
if (skip >= full_h)
|
||||||
|
return; /* обрезано целиком */
|
||||||
|
sy = skip;
|
||||||
|
h = full_h - skip;
|
||||||
|
if (rows > 0 && rows < h)
|
||||||
|
h = rows;
|
||||||
|
|
||||||
|
/* Клип по экрану — только если хоть один край вылезает. */
|
||||||
|
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
|
||||||
|
if (x < 0) { cx = -x; w += x; x = 0; }
|
||||||
|
if (y < 0) { sy -= y; h += y; y = 0; }
|
||||||
|
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
|
||||||
|
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. */
|
||||||
|
src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px;
|
||||||
|
if (cx)
|
||||||
|
src += cx * sstride;
|
||||||
|
src += sy;
|
||||||
|
|
||||||
|
_bgi_begin();
|
||||||
|
dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x);
|
||||||
|
_bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y);
|
||||||
|
_bgi_end();
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* gfx_set_idle_hook — что вызывать, пока gfx_wait_vsync ждёт луч.
|
||||||
|
*
|
||||||
|
* ЗАЧЕМ. Ожидание кадра — это самый длинный простой в кадре: у игры с
|
||||||
|
* пейсингом «3 растровых кадра на логический тик» процессор проводит там
|
||||||
|
* ~42 мс из 60, крутя `in a,(#0xFE)` и больше ничего не делая. Хук
|
||||||
|
* позволяет занять это время полезным опросом, не отнимая ни такта у
|
||||||
|
* полезной работы.
|
||||||
|
*
|
||||||
|
* Первый потребитель (и повод завести) — клавиатура: приёмный FIFO SIO
|
||||||
|
* всего 3 байта, а одно нажатие расширенной клавиши при зажатом Shift
|
||||||
|
* даёт пачку из 5 байт («fake shift»); если импульс запроса прерывания
|
||||||
|
* потерян, байты копятся и один теряется. Лечится вычерпыванием по
|
||||||
|
* опросу (`kbd_raw_poll` из <kbd_raw.h>) — но только ПЛОТНЫМ, раз в
|
||||||
|
* ~0.5 мс, а не пару раз за кадр. Замер: applications/PoP/roomtest/
|
||||||
|
* TASKS.md, задача KBD-1.
|
||||||
|
*
|
||||||
|
* ТРЕБОВАНИЯ К ХУКУ: он зовётся в тесном цикле, поэтому обязан быть
|
||||||
|
* дешёвым и НЕ рисовать (окно W3 в этот момент принадлежит вызывающему,
|
||||||
|
* а не графике) и не ждать сам. fn = 0 — снять хук.
|
||||||
|
*/
|
||||||
|
#include "../_bgi.h"
|
||||||
|
|
||||||
|
void gfx_set_idle_hook(void (*fn)(void))
|
||||||
|
{
|
||||||
|
_gfx_idle_hook = fn;
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ static void gfx_wait_vsync_beam(void)
|
|||||||
;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) ---
|
;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) ---
|
||||||
ld bc, #0 ; ~65536 попыток на фазу — таймаут
|
ld bc, #0 ; ~65536 попыток на фазу — таймаут
|
||||||
_gwv_wait1:
|
_gwv_wait1:
|
||||||
|
call _gwv_idle ; занять простой (см. gfx_set_idle_hook)
|
||||||
in a, (#0xFE)
|
in a, (#0xFE)
|
||||||
bit 5, a
|
bit 5, a
|
||||||
jr nz, _gwv_have1
|
jr nz, _gwv_have1
|
||||||
@@ -75,6 +76,7 @@ static void gfx_wait_vsync_beam(void)
|
|||||||
;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) ---
|
;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) ---
|
||||||
ld bc, #0
|
ld bc, #0
|
||||||
_gwv_wait0:
|
_gwv_wait0:
|
||||||
|
call _gwv_idle
|
||||||
in a, (#0xFE)
|
in a, (#0xFE)
|
||||||
bit 5, a
|
bit 5, a
|
||||||
jr z, _gwv_done
|
jr z, _gwv_done
|
||||||
@@ -83,6 +85,26 @@ static void gfx_wait_vsync_beam(void)
|
|||||||
or c
|
or c
|
||||||
jr nz, _gwv_wait0
|
jr nz, _gwv_wait0
|
||||||
;; таймаут и здесь — переходим к тому же фолбэку
|
;; таймаут и здесь — переходим к тому же фолбэку
|
||||||
|
jr _gwv_fallback
|
||||||
|
|
||||||
|
;; --- вызов idle-хука ------------------------------------- *
|
||||||
|
;; BC (счётчик таймаута) сохраняем: хук — обычная C-функция и
|
||||||
|
;; клобберит всё, кроме IX. Косвенный вызов через push адреса
|
||||||
|
;; возврата + jp (hl): `call (hl)` в Z80 нет. Цена холостого
|
||||||
|
;; прохода — около двух десятков тактов в цикле, который и так
|
||||||
|
;; сжигает время впустую.
|
||||||
|
_gwv_idle:
|
||||||
|
ld hl, (__gfx_idle_hook)
|
||||||
|
ld a, h
|
||||||
|
or l
|
||||||
|
ret z ; хук не поставлен
|
||||||
|
push bc
|
||||||
|
ld de, #_gwv_idle_ret
|
||||||
|
push de ; адрес возврата для jp (hl)
|
||||||
|
jp (hl)
|
||||||
|
_gwv_idle_ret:
|
||||||
|
pop bc
|
||||||
|
ret
|
||||||
|
|
||||||
_gwv_fallback:
|
_gwv_fallback:
|
||||||
ei
|
ei
|
||||||
|
|||||||