Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f3e854854 | |||
| 75a51fb1db | |||
| a32b66700f | |||
| 59e51f7e83 | |||
| b22cee3456 | |||
| 2e90eaf7d7 | |||
| 3983fa4513 | |||
| d014a3f577 | |||
| 4f7d9c0596 | |||
| 37fc572cc3 | |||
| 99b430f2ed | |||
| d438a1d3da | |||
| 79ea473910 | |||
| cf06896dbd | |||
| 8bbc6b4d07 | |||
| 565ba98852 | |||
| ac9871c58c | |||
| 1e6c377edc | |||
| 0280b05933 | |||
| f2093e0d89 | |||
| af5f0a4638 | |||
| 3dbad6120c | |||
| 1dc89b0f26 | |||
| e1ee447b7f | |||
| 18f5115e0d | |||
| b3bf2cca3d | |||
| 3b2dd8bfcc | |||
| ecf5ecfc14 | |||
| 4cffbc9aa4 | |||
| 24bb724c22 | |||
| eef6eebd8c | |||
| 3de8c7500c | |||
| 8e33cd07bc | |||
| a5773ab654 | |||
| 68b8fe5851 | |||
| 64ce6339eb | |||
| 1214785a56 | |||
| 63a25530f5 | |||
| fb495ace42 | |||
| e1846369ad | |||
| 1c91c5f92a | |||
| 6769b6a01b | |||
| 5f5eefc9fc | |||
| 517d225091 | |||
| fc69316c2c | |||
| 08ebf09d4d | |||
| 50aa652dbe | |||
| 08c6a8504f | |||
| 723da3c5c1 | |||
| e143ee010a | |||
| b106aba59b | |||
| 5e09bd9d5a | |||
| 6582154381 | |||
| c127a4b0d2 | |||
| 63a3b60524 | |||
| 86d7615841 | |||
| 3d586af031 | |||
| 95c22be9bd | |||
| d1bc97c589 | |||
| bf7ddfd4da | |||
| 7f3e32ddd6 | |||
| 21f978d441 | |||
| 8b30dc20c8 |
@@ -113,3 +113,10 @@ mame/
|
||||
|
||||
# Claude Code local settings (per-machine, not for the repo)
|
||||
.claude/
|
||||
|
||||
# Тяжёлые справочные материалы, НЕ версионируются: docs/extra — архивы
|
||||
# исходников (~570 МБ, четыре почти одинаковых bad_apple), docs/sources —
|
||||
# клоны чужих репозиториев (Sprinter-BIOS, Estex-DSS, SaymanNsk) со своими
|
||||
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
|
||||
docs/extra/
|
||||
docs/sources/
|
||||
|
||||
@@ -36,7 +36,9 @@ LIB := $(PROJ_ROOT)/lib/sprinter.lib
|
||||
|
||||
MAME_DIR := $(PROJ_ROOT)/mame/v306
|
||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
|
||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||
|
||||
# Optional knobs — see top of file.
|
||||
@@ -85,4 +87,14 @@ floppy: $(EXAMPLE).exe
|
||||
run: floppy
|
||||
cd $(MAME_DIR) && ./run_mame.sh
|
||||
|
||||
.PHONY: all clean floppy run
|
||||
# `make hdd` packs this program (+ optional EXTRA_DATA files) into the MAME
|
||||
# HDD image mounted as disk D: (-hard2 test_hdd.chd). Гораздо быстрее FDD —
|
||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||
hdd: $(EXAMPLE).exe
|
||||
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
@echo
|
||||
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
||||
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
||||
|
||||
.PHONY: all clean floppy run hdd
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Prince of Persia → ZX Sprinter — правила подпроекта
|
||||
|
||||
Порт Prince of Persia (DOS/Apple II) на Sprinter Sp2000 поверх нашего
|
||||
sprinter-cc / libc / libbgi. Действуют правила корневого
|
||||
`CLAUDE.md` (сборка, libc, ABI, MAME-автотест); ниже — только специфика PoP.
|
||||
Общение и комментарии — на русском.
|
||||
|
||||
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
||||
|
||||
**`SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ авторитетный
|
||||
источник того, как оригинальный движок это делает.** Правило без исключений:
|
||||
|
||||
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
||||
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
||||
константы) — СНАЧАЛА найди и прочитай соответствующий код в `SDLPoP/src/`,
|
||||
и портируй по нему. Не пиши по памяти, не выводи логику «из общих
|
||||
соображений», не угадывай значения — это источник багов, которые потом
|
||||
ловятся в MAME часами.
|
||||
2. **По любому вопросу «как в оригинале должно быть»** (что окклюдит что,
|
||||
в каком порядке слои, когда меняется тайл, какая скорость/задержка,
|
||||
что делает такой-то кадр анимации) — ответ ищи в `SDLPoP/src/`, а не
|
||||
строй гипотезу. Если в SDLPoP не нашёл — это повод копать дальше в
|
||||
исходнике, а не додумывать.
|
||||
3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока
|
||||
не доказано обратное (наша платформа/ABI требует отличия — тогда явно
|
||||
зафиксировать почему в комментарии).
|
||||
|
||||
Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/
|
||||
seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/
|
||||
слои/окклюзия, `seg009` чтение ресурсов. Слои окклюзии у нас = слои SDLPoP.
|
||||
См. memory `pop_check_sdlpop_first`.
|
||||
|
||||
Вторичные референсы (когда в SDLPoP непонятно/нужен другой ракурс):
|
||||
- `Prince-of-Persia-Apple-II/` — оригинальный 6502-исходник 1989 (Мехнер).
|
||||
- `PR/` (github.com/NagyD/PR, GPLv2) — Princed Resources.
|
||||
- `mininim/` — независимая реализация.
|
||||
|
||||
Все эти папки — **справочник логики/структур/констант и источник ассетов**,
|
||||
но НЕ код для копирования (лицензии несовместимы, наш ABI другой): читаем
|
||||
и переписываем под наш движок, а не вставляем куски.
|
||||
|
||||
## Ассеты
|
||||
|
||||
Готовые распакованные VGA-256 ассеты (то, что нужно под 320×256×256) —
|
||||
`SDLPoP/data/` (`res<id>.png`/`.pal`/`.bin`). Брать оттуда, а НЕ писать свой
|
||||
декодер DOS `.DAT`. Локальные `.DAT` — в `MSDOS/`.
|
||||
|
||||
**Каноническая спецификация форматов `.DAT` — `docs/POP-DAT-FormatSpecifications.pdf`**
|
||||
(грепаемая копия — `docs/POP-DAT-FormatSpecifications.txt`): первоисточник
|
||||
Princed для DAT v1.0 (контейнер/индекс/чек-сумма, кодеки RLE/LZG, палитры,
|
||||
формат уровней, звук), на нём построены и SDLPoP, и Princed Resources. Наши
|
||||
разборы (`docs/MSDOS_RESOURCE_FORMAT.md` / `docs/APPLEII_RESOURCE_FORMAT.md` /
|
||||
`docs/README.md`) — практические заметки/сверки; при расхождении источник
|
||||
истины — спецификация. Формат уровня почти идентичен в Apple II и DOS.
|
||||
|
||||
Упаковка ассетов под Sprinter (атласы `.atl`, палитра) — python-скрипты в
|
||||
`toolchain/` (`render_room.py`, `pop_pack_bg.py`, `pop_pack_kid.py`,
|
||||
`pop_extract_kid_data.py`). Их дёргают Makefile'ы тестов.
|
||||
|
||||
## Структура папки
|
||||
|
||||
- `docs/` — планы и форматы: `PORT_PLAN.md` (общий план фаз),
|
||||
`KID_PLAN.md`, `double_buffer_plan.md`, `loose_floors_plan.md`, форматы
|
||||
ресурсов. Начинать чтение отсюда.
|
||||
- `roomtest/` — **активная разработка**: комната 1 + Kid (анимация, ввод,
|
||||
коллизия, падение, зацеп, fore-окклюзия, loose-полы). Свой `CLAUDE.md`.
|
||||
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
||||
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
||||
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
||||
- `SDLPoP/`, `PR/`, `Prince-of-Persia-Apple-II/`, `mininim/`, `MSDOS/` —
|
||||
референсы/оригинальные данные (см. выше).
|
||||
|
||||
## Ключевые архитектурные решения (memory/)
|
||||
|
||||
- `pop_port_project` — общий статус порта.
|
||||
- `pop_banking_architecture` — будущее: big+BANK_W1, графику нельзя в W3,
|
||||
один файл = один банк = прямые вызовы, main резидентен.
|
||||
- `pop_background_strategy` — фон = композиция тайлов в рантайме (вариант 3).
|
||||
- `pop_kid_plan` / `pop_hang_state` / `pop_fore_layer` /
|
||||
`pop_fall_debug_baseline` — этапы Kid.
|
||||
- `kbd_raw_fifo_drain` — held-state клавиатуры (единственный принципиальный
|
||||
пробел движка, закрыт `<kbd_raw.h>`): вычерпывать FIFO SIO циклом.
|
||||
- `png_strip_padding_tradeoff`, `pop_tile_atlas_palette_merge` — квирки
|
||||
упаковки ассетов.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prince of Persia на ZX Sprinter
|
||||
|
||||
Порт Prince of Persia на компьютер Sprinter Sp2000 поверх нашего
|
||||
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
||||
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
||||
|
||||
Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
||||
Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
||||
|
||||
## Что где
|
||||
|
||||
| Папка | Назначение |
|
||||
|-------|-----------|
|
||||
| `roomtest/` | **Активная разработка.** Комната 1 уровня 1 живой композицией тайлов + Kid: анимация (seqtbl), управление с клавиатуры, коллизия, падение, зацеп/подтягивание, fore-окклюзия, проваливающиеся полы. Свой README/CLAUDE. |
|
||||
| `docs/` | Планы (`PORT_PLAN`, `KID_PLAN`, `double_buffer_plan`, `loose_floors_plan`) и разбор форматов ресурсов Apple II / DOS. |
|
||||
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
||||
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
||||
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
||||
| `SDLPoP/`, `PR/`, `mininim/` | Референсные реализации движка (GPL) — читаем логику/константы, НЕ копируем код. `SDLPoP/data/` — источник распакованных VGA-ассетов. |
|
||||
| `Prince-of-Persia-Apple-II/` | Оригинальный 6502-исходник 1989 г. |
|
||||
| `MSDOS/` | Локальные `.DAT`-ресурсы DOS-версии. |
|
||||
|
||||
## Референсы = только справочник
|
||||
|
||||
`SDLPoP/`, `PR/`, `mininim/`, `Prince-of-Persia-Apple-II/` используются как
|
||||
справочник структур/логики и как источник готовых ассетов — их код НЕ
|
||||
копируется в наш порт (лицензии несовместимы, ABI другой). Любая механика
|
||||
сверяется с `SDLPoP/src/` **до** реализации.
|
||||
|
||||
## Форматы ресурсов
|
||||
|
||||
Формат уровня почти идентичен в Apple II и DOS (2304 / 2305 байт,
|
||||
`blueprnt`). Графика различается принципиально, но брать распакованные PNG
|
||||
из `SDLPoP/data/` практичнее, чем декодировать сырой `.DAT`. Подробности —
|
||||
[`docs/README.md`](docs/README.md).
|
||||
@@ -8,8 +8,18 @@
|
||||
(например: смещение+размер последней записи таблицы точно совпадает с
|
||||
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
|
||||
|
||||
Для справки при последующей реализации (порт на ZX Sprinter) стоит держать в
|
||||
уме два внешних проекта:
|
||||
**Основной источник спецификации формата — `POP-DAT-FormatSpecifications.pdf`**
|
||||
(и его текстовая конверсия `POP-DAT-FormatSpecifications.txt` в этой же папке,
|
||||
для grep/цитирования): *«Prince of Persia — Specifications of File Formats»*,
|
||||
Princed Development Team, 2008 — каноническая спецификация формата `DAT v1.0`,
|
||||
на которой построен и SDLPoP, и Princed Resources. Разбирает контейнер, индекс,
|
||||
чек-сумму, кодеки изображений (RLE / LZG), палитры, формат уровней (room
|
||||
mapping, wall-drawing, room-linking, guards, start position, door events),
|
||||
звук (digital waves / MIDI / PC speaker), бинарные файлы и Mac-варианты. При
|
||||
любом расхождении между эмпирическими находками ниже и этим документом —
|
||||
источником истины считать спецификацию (сверять §-номера: её §3.x).
|
||||
|
||||
Дополнительно как справка при реализации (порт на ZX Sprinter):
|
||||
|
||||
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
|
||||
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Форматы ресурсов Prince of Persia — сводка
|
||||
|
||||
**Каноническая спецификация форматов** — `POP-DAT-FormatSpecifications.pdf`
|
||||
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
|
||||
*«Prince of Persia — Specifications of File Formats»*, Princed Development Team,
|
||||
2008. Это первоисточник формата `DAT v1.0` (контейнер, индекс, чек-сумма,
|
||||
кодеки RLE/LZG, палитры, уровни, звук), на котором построены и SDLPoP, и
|
||||
Princed Resources. Документы ниже — наши практические заметки/сверки; при
|
||||
расхождении источником истины считать спецификацию.
|
||||
|
||||
Цель этих документов — подготовить почву для будущего порта Prince of Persia
|
||||
на ZX Sprinter, разобрав, как устроены ресурсы игры в двух доступных нам
|
||||
версиях:
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# План: порт `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`.
|
||||
@@ -0,0 +1,264 @@
|
||||
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
|
||||
|
||||
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ
|
||||
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст).
|
||||
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
||||
|
||||
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
||||
перед кодингом читать соответствующий код seg*.c, не гадать.
|
||||
|
||||
---
|
||||
|
||||
## 0. КОНТЕКСТ: текущее состояние `applications/PoP/roomtest` (что уже готово)
|
||||
|
||||
roomtest — живой прототип порта PoP: комната 1 уровня 1 живой композицией
|
||||
тайлов + Kid (анимация/управление/коллизия/падение/зацеп/переходы). Собрать:
|
||||
`cd applications/PoP/roomtest && make`. Тест в MAME: см. memory
|
||||
`mame_mcp_bridge`/`mame_hdd_test_disk` (канонический цикл: `make` → пересобрать
|
||||
`mame/v306/IMG/test_hdd.chd` через `toolchain/make_hdd.sh` со всеми ассетами →
|
||||
рестарт `run_bridge.sh` → `resume` → ~13с бут → `type_string("d:{ENTER}roomtest.exe{ENTER}")`).
|
||||
Отладка: клавиши `1`=freeze / `2`=resume в roomtest; MCP-мост `mame-z80`
|
||||
(read_logical_memory, set_breakpoint, disassemble); адреса символов —
|
||||
`.sprinter-cc-roomtest/roomtest.map` (сдвигаются при пересборке!).
|
||||
|
||||
### Модули (все в `applications/PoP/roomtest/`)
|
||||
- `roomtest.c` — главный цикл (дабл-буфер 2 стр.), `enter_room(room)`,
|
||||
обработчики переходов. File-static рабочие массивы (W2):
|
||||
`room_fg[30]`, `room_bg[30]`, `lcol_fg/lcol_bg[3]`, `rcol_fg/rcol_bg[3]`,
|
||||
`below_fg[10]`, `cur_room`.
|
||||
- `pop_level.c/.h` — **уровень из файла** (Фаза L1):
|
||||
- `pop_level_load("res2001.bin")` — читает сырой blueprnt в EMM-страницу
|
||||
(данные с offset `0x100`, ISR-стаб как атлас).
|
||||
- `pop_room_load(room, fg,bg, lcol_fg,lcol_bg, rcol_fg,rcol_bg, below_fg)` —
|
||||
извлекает комнату (fg маскирован `&0x1F`, bg raw) + срезы соседей:
|
||||
leftcol=col9 левого соседа, rightcol=col0 правого, belowrow=row0 нижнего.
|
||||
- `pop_room_link(room, side)` — связь (side 0=L,1=R,2=U,3=D; 0=нет).
|
||||
- `pop_level_start_room/pos/dir()`.
|
||||
- `pop_bg.c/.h` — отрисовка тайлов (порт seg008 draw_tile), fore-окклюзия над
|
||||
Kid (`pop_fore_over_kid`, порт set_char_collision+redraw_at_char/char2),
|
||||
**loose-полы** (shake/bake/mob). `draw_tile` — статическая, знает
|
||||
`draw_gate_back` (грань гейта из левой комнаты).
|
||||
- `pop_kid.c/.h` — анимация Kid (интерпретатор seqtbl `play_seq`, порт seg006),
|
||||
`Kid` struct (frame,x,y,dir,curr_col,curr_row,action,fall_x,fall_y,repeat,
|
||||
curr_seq); `knock`-флаг; `kid_cur_dx/flags`, `kid_fp_*` (футпринт).
|
||||
- `pop_ctrl.c/.h` — ввод (порт seg005 control) через `<kbd_raw.h>`.
|
||||
- `pop_map.c/.h` — коллизия/физика (порт seg005/006). Ключевое:
|
||||
- `pop_map_set(fg)` — карта текущей комнаты.
|
||||
- `pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg)` — связи + кромки швов для
|
||||
коллизии (`get_tile(col=-1)`=lcol, `get_tile(col=10)`=rcol; порт
|
||||
find_room_of_tile).
|
||||
- `pop_phys_tick()` — кадр физики (fall/land/wall/knock/leave).
|
||||
- Переходы: `pop_fell_out` (вниз, y>=211), `pop_leave_dir` (1=left,2=right,
|
||||
x∓140).
|
||||
- **loose-состояние**: `pop_loose_modif[30]` (публично, читает pop_bg),
|
||||
`loose_bake[30]`, `loose_rest[30]` (static); `pop_loose_tick()`,
|
||||
`pop_loose_reset()` (сброс при смене комнаты).
|
||||
|
||||
### Что сделано по фазам
|
||||
- **L1** — данные уровня из файла (room1_data.h удалён). Коммит `21f978d`.
|
||||
- **L2** — переход в комнату снизу (провал/спуск), фиксы окклюзии. Коммит `7f3e32d`.
|
||||
- **L3** — переходы вбок (право+лево) через швы. **Не закоммичено** на момент
|
||||
написания (вместе с этим планом). **L3-вверх (climb-up в комнату сверху) —
|
||||
НЕ сделано.**
|
||||
- **Loose-полы** (тряска knock / падение mob+окклюзия) — коммит `8b30dc2`.
|
||||
|
||||
### Известные ОГРАНИЧЕНИЯ (важно для этого плана)
|
||||
1. **Нет персистентности тайлов**: `enter_room`→`pop_room_load` каждый раз
|
||||
перезагружает ИСХОДНЫЕ тайлы из level-страницы. Изменения (упавший loose,
|
||||
открытый гейт) при повторном входе ТЕРЯЮТСЯ. Для кнопок/гейтов это
|
||||
блокер (см. P0).
|
||||
2. **Нет HP/смерти** Кида (нужно для пик).
|
||||
3. Loose-механика — частный случай trob (нужно обобщить).
|
||||
|
||||
---
|
||||
|
||||
## 1. ДАННЫЕ УРОВНЯ (формат, offsets, объекты)
|
||||
|
||||
Сырой `res2001.bin` (2305 Б) = blueprnt DAT 1.0 (Table 6 в
|
||||
`POP-DAT-FormatSpecifications.txt`). Читается в EMM-страницу с offset `0x100`.
|
||||
Тайл-код = байт `& 0x1F`; верхние биты (модификатор BLUETYPE) сейчас отброшены.
|
||||
|
||||
| Блок | Offset | Размер |
|
||||
|------|--------|--------|
|
||||
| foretable (fg) | 0 | 720 (24 комн × 30) |
|
||||
| backtable (bg=modifier) | 720 | 720 |
|
||||
| **LINKLOC** (doorlink1) | **1440** | 256 |
|
||||
| **LINKMAP** (doorlink2) | **1696** | 256 |
|
||||
| links (roomlinks) | 1952 | 96 (24×{L,R,U,D}) |
|
||||
| start_position | 2112 | 3 (room,pos,dir) |
|
||||
|
||||
Тайл-коды: `0x00`empty `0x01`floor `0x02`**SPIKE** `0x03`pillar `0x04`**GATE**
|
||||
`0x06`**DROP-кнопка(closer)** `0x0B`loose `0x0F`**RAISE-кнопка(opener)**
|
||||
`0x10`lvldoor-L `0x11`lvldoor-R `0x13`torch `0x14`wall.
|
||||
|
||||
### Объекты уровня 1 (по комнатам)
|
||||
```
|
||||
room 5: DROP(0,2)m11 RAISE(0,4)m9 GATE(0,5)m2 RAISE(0,6)m8 GATE(0,9)m1
|
||||
room 6: RAISE(0,2)m7 SPIKE(2,3) SPIKE(2,4) <-- тестовая
|
||||
room 7: RAISE(0,2)m6 GATE(0,9)m2
|
||||
room 8: RAISE(0,6)m5 GATE(0,9)m2 RAISE(1,7)m4
|
||||
room 9: RAISE(0,0)m3 (+ lvldoor(1,3)/(1,4) — выход на level2, отложено)
|
||||
room12: RAISE(0,3)m2 GATE(0,9)m2 SPIKE(2,4)
|
||||
room10/13/14/16/19/24: только SPIKE
|
||||
room20: DROP(1,4)m1 RAISE(1,7)m0
|
||||
```
|
||||
(m = modifier тайла = ИНДЕКС в LINKLOC/LINKMAP.)
|
||||
|
||||
### Room6 (тестовая) — раскладка
|
||||
```
|
||||
fg row0: 13 01 0F 00 03 01 01 13 01 03 (0,2)=RAISE-кнопка, (0,0)/(0,7)=torch
|
||||
fg row1: 14 14 14 00 14 14 14 14 14 14 (1,3)=empty
|
||||
fg row2: 14 14 14 02 02 14 14 14 14 14 (2,3)(2,4)=SPIKE
|
||||
links: L=8 R=2 U=5 D=0
|
||||
```
|
||||
- **Шахта пик**: col3 (row0=empty, row1=empty, row2=spike) + col4 (spike).
|
||||
- **Гейт, видимый у ЛЕВОЙ кромки room6, — это гейт room8 (0,9)**, отрисованный
|
||||
в col0 room6 через левый шов (L=8, leftcol=col9 room8). В room6 гейта НЕТ.
|
||||
|
||||
### Связь кнопка→цель (декод LINKLOC/LINKMAP), проверено:
|
||||
- `get_doorlink_tile(i) = d1[i] & 0x1F`
|
||||
- `get_doorlink_next(i) = !(d1[i] & 0x80)` (0 = конец цепочки)
|
||||
- `get_doorlink_room(i) = ((d1[i]&0x60)>>5) + ((d2[i]&0xE0)>>3)`
|
||||
- `get_doorlink_timer(i) = d2[i] & 0x1F`
|
||||
- где `d1`=LINKLOC@1440, `d2`=LINKMAP@1696. Цепочка: idx++ пока next.
|
||||
|
||||
**Кнопка room6 (0,2) mod=7 → цель: room8 tile(0,9) = ГЕЙТ.** Подтверждено
|
||||
в SDLPoP-скринах: нажатие кнопки поднимает решётку у левой кромки room6.
|
||||
Связь КРОСС-КОМНАТНАЯ (кнопка в room6, гейт в room8) и задаётся таблицей,
|
||||
а НЕ позицией. Кнопка может открывать НЕСКОЛЬКО гейтов в разных комнатах.
|
||||
|
||||
---
|
||||
|
||||
## 2. МЕХАНИКА SDLPoP (точные ссылки)
|
||||
|
||||
### 2.1 Trob-система (анимируемые тайлы)
|
||||
- `add_trob(room,tilepos,type)` seg007:0A5A — в список анимируемых.
|
||||
- Каждый кадр `redraw_needed_tiles`/`process_trobs` продвигает; диспетч по
|
||||
типу тайла → `animate_button/animate_door/animate_spike/animate_loose`
|
||||
(seg007:0033+ таблица `animate_*`).
|
||||
- Состояние тайла хранится в `curr_room_modif[tilepos]` (per-room modifier).
|
||||
- У нас есть частный случай для loose (`pop_loose_tick`+`pop_loose_modif[30]`).
|
||||
|
||||
### 2.2 Пики (spike)
|
||||
- **Триггер выдвижения** — `check_spike_below()` seg006:1199 (зовётся в
|
||||
физике Кида каждый кадр): для каждой колонки футпринта Кида
|
||||
(`get_tile_div_mod_m7(char_x_left)`..`char_x_right`) идёт ВНИЗ от
|
||||
`Char.curr_row` через НЕ-floor тайлы; если встретил `tiles_2_spike` →
|
||||
`start_anim_spike(room,tilepos)`. → Кид у края (0,2) правым краём задевает
|
||||
col3 → скан вниз col3 (empty/empty/spike) → пики вылезают.
|
||||
- `start_anim_spike` seg007:596: если `modif<=0`: `modif==0` → add_trob(type1)
|
||||
+ звук; `modif<0` (кроме 0xFF disabled) → `modif=0x8F`.
|
||||
- `animate_spike` seg007:317: автомат по modif — выдвиг `++modif` (1..4; на 5 →
|
||||
`0x8F`; на 9 → 0, trob кончился); убирание `& 0x80` → `--modif` (на 0 → `=6`).
|
||||
`0xFF` = disabled (не двигать).
|
||||
- `is_spike_harmful` seg007:1178: modif `0/-1`→0 (безопасно); `<0`→1;
|
||||
`1..4`→2; `>=5`→0.
|
||||
- **Смерть**: `check_spiked` seg006:0968 — если тайл под Кидом = spike И harmful
|
||||
И кадр бега (7..14) / старта прыжка (34..39) с harmful>=2, ИЛИ кадр приземления
|
||||
(43/26) с harmful!=0 → `spiked()`. Падение на пики — отдельный путь (см.
|
||||
`is_dead` seg006:1907, frame_177_spiked). Осторожный ШАГ по невыдвинутым — ок.
|
||||
|
||||
### 2.3 Кнопки
|
||||
- `trigger_button(playsound, button_type, modifier)` seg007:0C53: modifier =
|
||||
индекс LINKLOC. `link_timer = get_doorlink_timer(mod)`; если `!=0x1F`
|
||||
(не заклинено): `set_doorlink_timer(mod,5)`; если был `<2` → `add_trob`
|
||||
(кнопка нажимается) + звук; затем `do_trigger_list(mod, button_type)`.
|
||||
- `do_trigger_list` seg007:09E5: идёт по цепочке LINKLOC от idx, для каждой
|
||||
цели `trigger_1(target_type,room,tilepos,button_type)` → если >=0
|
||||
`add_trob(room,tilepos,result)`.
|
||||
- `animate_button` seg007:0D3A: `timer=get_doorlink_timer(mod)-1`;
|
||||
`set_doorlink_timer(mod,timer)`; `timer<2` → кнопка отжимается.
|
||||
- Когда Кид ВСТАЁТ на кнопку: `check_press`-путь seg006 (opener → trigger_button,
|
||||
closer → тоже; если Кид мёртв — `died_on_button`). RAISE=`tiles_15_opener`
|
||||
(0x0F), DROP=`tiles_6_closer` (0x06).
|
||||
|
||||
### 2.4 Гейты
|
||||
- `trigger_1` seg007:0999 → для `tiles_4_gate` → `trigger_gate`.
|
||||
- `trigger_gate(room,tilepos,button_type)` seg007:092C: modif = высота открытия.
|
||||
opener: `0xFF`→игнор; `>=188`(открыт)→держать `238`; иначе `modif=(modif+3)&0xFC`,
|
||||
return 1 (открывать). closer/иначе: `modif!=0` → return 3 (закрыть быстро).
|
||||
- `animate_door` seg007:0522: анимация открытия/закрытия; `door_delta[]={-1,4,4}`,
|
||||
`gate_close_speeds[]={0,0,0,20,40,60,80,100,120}`. Гейт медленно закрывается
|
||||
после истечения таймера кнопки.
|
||||
- Отрисовка: кадры гейта; у нас `draw_gate_back` в pop_bg (грань из левой комн.).
|
||||
- **Проходимость**: Кид блокируется недостаточно открытым гейтом (коллизия
|
||||
как стена, порог по высоте открытия); проходит при `modif` открытом.
|
||||
|
||||
---
|
||||
|
||||
## 3. Поправки к описанию пользователя (что важно)
|
||||
|
||||
1. Гейт НЕ в room6 (0,0) — он в **room8 (0,9)**, виден через левый шов; связь
|
||||
кросс-комнатная (по таблице LINKLOC, не по позиции).
|
||||
2. Кнопка может открывать несколько гейтов в разных комнатах.
|
||||
3. Пики выдвигаются по `check_spike_below` (Кид над колонкой с пиками),
|
||||
имеют состояния (не всегда смертельны), убираются со временем.
|
||||
4. **Кросс-комнатное состояние тайлов ДОЛЖНО ПЕРСИСТИТЬ** — блокер (см. P0).
|
||||
5. HP/смерть — новая подсистема.
|
||||
6. Кнопка сама анимируется (нажата/отжата).
|
||||
|
||||
---
|
||||
|
||||
## 4. ПЛАН РЕАЛИЗАЦИИ (фазы)
|
||||
|
||||
### P0 — Персистентное per-room modifier-состояние + trob-каркас (ПРЕРЕКВИЗИТ)
|
||||
**Проблема:** нажатие кнопки в room6 меняет modif гейта room8 (не текущей
|
||||
комнаты); при входе в room8 нужно отрисовать гейт в текущем состоянии. Плюс
|
||||
это чинит «re-entry восстанавливает тайлы» (loose/гейты).
|
||||
|
||||
Дизайн (предложение — уточнить в реализации):
|
||||
- Массив `room_modif[24][30]` (или lazy per-visited-room) — modifier каждого
|
||||
тайла каждой комнаты. Инициализируется из bg уровня при первой загрузке
|
||||
комнаты; далее ЖИВЁТ (не перезагружается). ~720 Б — влезает в W2 (или в
|
||||
EMM-страницу уровня рядом с данными: остаётся >10КБ).
|
||||
- `enter_room` берёт modif из `room_modif[room]`, а fg — из level-страницы
|
||||
(fg почти не меняется; исключения — loose→empty, надо тоже персистить: либо
|
||||
отдельный `room_fg_override`, либо флаг «loose упал»).
|
||||
- Обобщить loose-trob: единый список trob (room,tilepos,type) + диспетчер
|
||||
`animate_*` по коду тайла. Loose (`pop_loose_*`) — перевести на него.
|
||||
- Кросс-комнатный trigger: `add_trob` в НЕ текущую комнату меняет
|
||||
`room_modif[room][tilepos]`; анимация продвигается даже для невидимой комнаты
|
||||
(в оригинале — да; можно упростить: для невидимой комнаты гейт сразу в
|
||||
финальном состоянии, анимировать только при видимости — решить при реализации).
|
||||
|
||||
### Фаза S — Пики (самодостаточно; отладит HP/смерть)
|
||||
Порядок:
|
||||
1. `room_modif` для пик (из P0 или временно локально).
|
||||
2. `check_spike_below` (порт seg006:1199) — в `pop_phys_tick`.
|
||||
3. `start_anim_spike` + `animate_spike` (порт seg007) — состояние в modif.
|
||||
4. `is_spike_harmful` + `check_spiked` (порт seg006:0968).
|
||||
5. **HP/смерть**: ввести `hitp_curr` (старт напр. 3); `take_hp`; при пиках —
|
||||
мгновенная смерть; seq смерти (`seq_22_crushed`/`frame_177_spiked`..185);
|
||||
анимация смерти; респавн (kid_init на старте или чек-поинт).
|
||||
6. Отрисовка: кадры выдвижения пик. В pop_bg есть `SPIKES_FRAM_RIGHT` — нужны
|
||||
pop-out кадры (spikes_fram по modif) + fore над Кидом.
|
||||
|
||||
### Фаза B — Кнопки + гейты (нужен P0)
|
||||
Порядок:
|
||||
1. Доступ к LINKLOC/LINKMAP из pop_level (добавить геттеры doorlink1/2[i] с
|
||||
маппингом W0 или скопировать таблицы в W2 при load — 512 Б).
|
||||
2. `pop_map` детект «Кид встал на кнопку» (check_press-путь) → `trigger_button`.
|
||||
3. `trigger_button` → таймер + `do_trigger_list` (обход цепочки) →
|
||||
`trigger_gate` для целей → изменить `room_modif[целевой]`.
|
||||
4. `animate_button` (кнопка отжимается) + `animate_door` (гейт откр/закр +
|
||||
авто-закрытие).
|
||||
5. Отрисовка гейта (кадры по modif) через ЛЕВЫЙ шов (гейт room8 в room6) +
|
||||
при входе в room8. Расширить `draw_gate_back`/добавить `draw_gate`.
|
||||
6. Коллизия: закрытый гейт = стена (порог по высоте открытия); открытый —
|
||||
проход. Учесть кросс-комнатный гейт на шве (проход влево room6→room8).
|
||||
|
||||
**Порядок фаз:** P0 → S → B. S в основном независим (кроме HP/trob-каркаса),
|
||||
но проще и отладит смерть/анимацию; B требует P0 (кросс-комнатное состояние).
|
||||
|
||||
---
|
||||
|
||||
## 5. Открытые вопросы / грабли
|
||||
- Персистентность `fg` для loose (тайл→empty): решить в P0 (override-массив или
|
||||
флаг), иначе упавший loose «вернётся».
|
||||
- Анимация trob в НЕВИДИМОЙ комнате: упростить (финальное состояние сразу) или
|
||||
портировать честно.
|
||||
- Двоебуфер: любой транзиент (пики/гейт/кнопка) финализировать перерисовкой
|
||||
«покоя» на ОБЕИХ страницах (урок из loose — см. memory `pop_loose_floors`).
|
||||
- Дверь уровня (lvldoor room9) + переход на level 2 — ОТДЕЛЬНО, отложено.
|
||||
- L3-**вверх** (climb-up в комнату сверху) — ещё не сделан; можно закрыть до
|
||||
объектов или параллельно.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Идеи и вопросы «на подумать» (PoP)
|
||||
|
||||
Не план работ, а список того, что осознанно отложено: каждая запись —
|
||||
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
||||
сделать это прямо сейчас.
|
||||
|
||||
## Заменить генератор псевдослучайных чисел
|
||||
|
||||
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
|
||||
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
|
||||
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
|
||||
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
|
||||
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
|
||||
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
|
||||
|
||||
## Отключать мышь на время игры
|
||||
|
||||
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
|
||||
см. memory `mouse_api`). Игре она не нужна вообще: управление —
|
||||
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
|
||||
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
|
||||
который у нас и без того занят на 86 %.
|
||||
|
||||
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
|
||||
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
|
||||
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
|
||||
кадров подряд прошли без единого превышения. То есть эффект реальный и
|
||||
измеримый, просто в тесте он был наведён извне.
|
||||
|
||||
**Что проверить.**
|
||||
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
|
||||
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
|
||||
«hide cursor» и снимает ли она обработчик.
|
||||
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
|
||||
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
|
||||
3. Не ломает ли отключение выход в DSS: состояние обязано
|
||||
восстанавливаться при `exit`, включая аварийный (atexit).
|
||||
|
||||
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
|
||||
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
|
||||
после выхода — заметный. Делать после того, как закроем стражей и
|
||||
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||||
возможный возврат `pop_bg` в резидент `--w3`).
|
||||
@@ -0,0 +1,446 @@
|
||||
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
||||
|
||||
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
|
||||
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
|
||||
— рассчитан на старт с пустого контекста.
|
||||
|
||||
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
||||
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
|
||||
ограничения окна W3.
|
||||
|
||||
---
|
||||
|
||||
## СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)
|
||||
|
||||
| Шаг | Статус | Факт |
|
||||
|-----|--------|------|
|
||||
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
|
||||
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | −41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
|
||||
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
|
||||
| 4. Разгрузка/перебалансировка W3 | **сделан** | резидент = `pop_bg` + `pop_gdraw` (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б) |
|
||||
| 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
|
||||
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
|
||||
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
|
||||
|
||||
**Замер сейчас против замера §1:** `_CODE` 24 881 → 24 421, куча W2 2 076 → 2 592 Б,
|
||||
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
|
||||
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
|
||||
|
||||
**Замер 2026-07-29 (после стража).** Появление стража съело кучу до 805 Б;
|
||||
разгрузка — вынос ОТРИСОВКИ стража в резидент (`pop_gdraw.c`, `--w3`), логика
|
||||
и состояние остались в W1/W2, чтобы банк `guards.c` их видел (R2). Итог:
|
||||
`_CODE` 26 149 → 25 703, куча **805 → 1 245 Б**, W3-резидент 11 656 → 12 819
|
||||
(свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент»
|
||||
теперь формулируется одним правилом: **резидент = только то, что рисует и
|
||||
зовётся исключительно из главного цикла**; всё, что может понадобиться банку,
|
||||
остаётся в W1/W2.
|
||||
|
||||
### Что сделано вместо §5.3 (вынос loose в W3)
|
||||
|
||||
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
|
||||
**порт архитектуры оригинала**: логика ставит пометку, отрисовка идёт
|
||||
отдельным проходом — `set_redraw_*` (seg007) + `redraw_needed` (seg008:0178).
|
||||
Появился `pop_redraw.c/.h`; `pop_trob` и `pop_map` больше не рисуют тайлы.
|
||||
Наши самодельные счётчики (`spike_rest`, `button_rest`, `ldoor_rest`,
|
||||
`loose_bake`, `loose_rest`, `ceil_rest`, `ceil_bake`, `land_bake`) удалены —
|
||||
их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.
|
||||
|
||||
**Два исключения остались** (обе — функции ТОЛЬКО главного цикла, звать из
|
||||
банка нельзя):
|
||||
- `pop_process_trobs` — пламя факела и пузырёк зелья (покадровый оверлей);
|
||||
- `pop_loose_tick` — падающий кусок (mob): spawn/tick/pos. В оригинале это
|
||||
отдельная подсистема (`mobs` + `draw_moving`), разделение на логику и
|
||||
отрисовку — задел следующей фазы.
|
||||
|
||||
### Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА
|
||||
|
||||
`tests/w3bankgfx` (huge + `--w3 res.c` + `--bank 1=bank1.c`, графика 256):
|
||||
|
||||
- банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри
|
||||
банка до блита, после блита и после возврата из вызванной им W1/W2-функции
|
||||
одна и та же (0xF0), резидент — 0xF3. То есть `_bgi_begin`/`_bgi_end`
|
||||
корректно возвращают ИМЕННО банковую страницу (правило R4);
|
||||
- вызов W1/W2-функции из банка работает, и она тоже может рисовать;
|
||||
- резидент W3 жив и вызывается после возврата из банка (R3).
|
||||
|
||||
**Дополнительно выяснено (важно для стражей):** писучие статики
|
||||
`__banked`-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не
|
||||
прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё
|
||||
состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в
|
||||
W1/W2 как обычные глобалы, а банк — только код.
|
||||
|
||||
Ещё одна мина, найденная там же: инлайновый `in a,(#0xE2)` посреди тела
|
||||
функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет
|
||||
заливки стал номером страницы, и «резидент не рисовал»). Читать порт
|
||||
отдельной `__naked`-функцией.
|
||||
|
||||
### Найденная по дороге ловушка компилятора
|
||||
|
||||
`(const T *)КОНСТАНТА + var*K` SDCC 4.5 может собрать неверно: умножение
|
||||
делает в 16 битах, а потом берёт только младший байт (`ld c,l` / `inc b`).
|
||||
Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага
|
||||
пропадал `FRAME_NEEDS_FLOOR` и персонаж проваливался сквозь пол. Лечение —
|
||||
считать адрес в `uint16_t` и кастовать один раз. Тот же паттерн в
|
||||
`pop_level.c` компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа
|
||||
работает» нельзя. Подробности: memory `sdcc_z80_const_ptr_index_bug`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Что уже сделано из v1 (не повторять)
|
||||
|
||||
- `--opt-code-size` и `--max-allocs 100000` **уже включены по умолчанию** в
|
||||
`bin/sprinter-cc` (v1 §2.1 закрыт, выигрыш получен).
|
||||
- Лишние блиты переднего слоя убраны (v1 §7 п.0): `fore_tile` больше не рисует
|
||||
`bottom_id`, `_CODE` −388 Б.
|
||||
- `gfx_blit_noclip` в libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.
|
||||
- `--w3` как резидент окна 3 реализован и обкатан (memory `w3_resident_code`).
|
||||
|
||||
---
|
||||
|
||||
## 1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)
|
||||
|
||||
Команда: `--memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c`.
|
||||
|
||||
### 1.1 Окна
|
||||
|
||||
| Область | Занято | Свободно | Примечание |
|
||||
|---|---|---|---|
|
||||
| W1+W2 `_CODE` | 24 881 Б | — | 0x4100…0xA231 |
|
||||
| W1+W2 `_HOME`+`_GSINIT`+`_DATA`+`_BSS` | ~4 240 Б | — | до 0xB2E4 |
|
||||
| **W1+W2 куча** | 0 (никто не malloc'ит) | **2 076 Б** | 0xB2E4…0xBB00 |
|
||||
| W1+W2 стек | — | 1 279 Б | 0xBB00…0xBFFE |
|
||||
| **W3 резидент** | 14 376 Б | **2 008 Б** | 0xC000…0xF828 |
|
||||
| EMM-страницы | 37 атласов + 1 уровень | ~215 страниц свободно | `sprinter_emm_budget` |
|
||||
|
||||
**Итого запаса до стены: ≈ 4 КБ** (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда
|
||||
не влезут.
|
||||
|
||||
### 1.2 Код по модулям (точно, из `.rel`)
|
||||
|
||||
```
|
||||
W1/W2 (_CODE 24 881): W3 резидент (_W3CODE 14 376):
|
||||
pop_kid 6 963 pop_bg 11 643
|
||||
pop_map 6 157 pop_trob 2 733
|
||||
roomtest 2 583
|
||||
pop_level 1 425
|
||||
pop_ctrl 1 145
|
||||
crt0 333
|
||||
libc+libbgi ~6 275
|
||||
```
|
||||
|
||||
### 1.3 Крупнейшие функции/данные (из `.lst`)
|
||||
|
||||
```
|
||||
pop_bg : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
|
||||
mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
|
||||
fore_only_tile 409, climb_overlay_tile 391, tile_table 371
|
||||
pop_map : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
|
||||
get_tile 266, do_knock 243, check_press 238, check_leave 212
|
||||
pop_kid : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
|
||||
(в _CODE!), собственно кода ~3.2 КБ
|
||||
roomtest : enter_room+main ~2.1 КБ
|
||||
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)
|
||||
```
|
||||
|
||||
### 1.4 `_DATA` (3 710 Б)
|
||||
|
||||
```
|
||||
pop_trob 963 (room_modif[24][30] = 720 + trobs + rest-массивы)
|
||||
pop_level 515 (копии LINKLOC/LINKMAP уровня)
|
||||
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
|
||||
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200
|
||||
```
|
||||
|
||||
### 1.5 Находки замера (мелкие, но чинить)
|
||||
|
||||
1. **`--w3` берёт ОДИН файл на флаг.** В `Makefile` написано
|
||||
`--w3 pop_trob.c pop_map.c --w3 pop_bg.c`, и это значит «W3 = pop_trob и
|
||||
pop_bg», а `pop_map.c` компилируется как обычный исходник в W1/W2. Судя по
|
||||
`.sprinter-cc-roomtest/w3_pop_map.rel` (устаревший артефакт), когда-то
|
||||
pop_map был в W3. **Решить осознанно** (см. §4) и записать явно:
|
||||
`--w3 pop_trob.c --w3 pop_bg.c`.
|
||||
2. `libc` тянет `_irq_state` 818 Б + `_kbdraw_state` 515 Б в `_DATA`.
|
||||
`__irq_vec_buf` (513 Б) — таблица векторов IM2; `__kbdraw_down` (512 Б) —
|
||||
битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ
|
||||
в самом дефицитном окне.
|
||||
|
||||
---
|
||||
|
||||
## 2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА
|
||||
|
||||
Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по
|
||||
`bin/sprinter-cc` (справка `--w3`/`--bank`) и по коду libbgi.
|
||||
|
||||
**(R1) Резидент W3 (`--w3`) и банки W3 (`--bank`) делят одно окно.**
|
||||
Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова
|
||||
`__banked` подменяет страницу W3 на банковую и возвращает резидентную назад.
|
||||
|
||||
**(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже.**
|
||||
Пока исполняется банк, резидентной страницы в адресном пространстве нет.
|
||||
Значит нельзя не только `bank → pop_bg()`, но и `bank → pop_map() → pop_bg()`.
|
||||
**Это ключевое ограничение при выборе, что делать банком.**
|
||||
|
||||
**(R3) Резидент → банк работает** (через трамплин в W1), резидент → W1/W2 —
|
||||
тоже.
|
||||
|
||||
**(R4) Графические примитивы libbgi звать можно откуда угодно.**
|
||||
`_bgi_begin` читает текущую страницу W3 из порта 0xE2, а `_bgi_end` её
|
||||
возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя
|
||||
только одно: **звать `_bgi_begin`/`_bgi_end` ИЗ кода, который сам лежит в W3**
|
||||
(после подмены страницы исчезнет исполняемый код — проверено, белый экран).
|
||||
Поэтому `pop_bg` (резидент W3) обязан пользоваться готовыми примитивами
|
||||
(`gfx_blit*`, `bar`, …), а батчинг скобки на весь `draw_tile` (v1 §8 шаг 1)
|
||||
для него **невозможен** без переноса самого `draw_tile` в W1/W2.
|
||||
|
||||
**(R5) `--w3` кладёт в W3 код И rodata модуля** (`--codeseg/--constseg
|
||||
W3CODE`), а писучие статики оставляет в `_DATA` (W2). То есть `const`-таблицы
|
||||
переносятся в W3 бесплатно вместе с модулем (так уже лежит `tile_table` 371 Б).
|
||||
|
||||
**(R6) Данные в EMM-странице читаются, только пока страница в окне.**
|
||||
`gfx_w0_map(page)` / `gfx_w0_unmap()` — окно W0 (0x0000…0x3FFF), первые 0x100
|
||||
занимает ISR-стаб. Так уже работает `pop_level`. Цена — пара `OUT` на
|
||||
маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем
|
||||
цикле.
|
||||
|
||||
---
|
||||
|
||||
## 3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)
|
||||
|
||||
- **Нельзя банковать `pop_bg`.** Он вызывается из pop_map, pop_trob, roomtest,
|
||||
pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит
|
||||
`tile_table` и всю отрисовку. Банк дал бы трамплин на каждый блит.
|
||||
- **Нельзя банковать `pop_map`, пока `pop_map` зовёт `pop_bg`** (R2). Сейчас
|
||||
зовёт: `pop_loose_tick` и компания (~30 вызовов графики).
|
||||
- **Нельзя тащить `kid_frames` в EMM «в лоб»**: он читается несколько раз за
|
||||
кадр из коллизии (`kid_cur_dx`/`kid_cur_flags` → `dx_weight`,
|
||||
`char_x_forward_edge`, …). Нужен кэш кадра (см. §5.1) — иначе маппинг
|
||||
страницы окажется в горячем пути.
|
||||
- **Нельзя «причёсывать» семейство `draw_tile` ради экономии, не имея
|
||||
пиксельного теста.** Мы неделю выравнивали слои по SDLPoP; любой рефактор
|
||||
этой зоны проверять диффом страниц (заморозка кадра клавишей `1` + сравнение
|
||||
VRAM обеих страниц, приём из memory `mame_mcp_bridge`).
|
||||
|
||||
---
|
||||
|
||||
## 4. ЦЕЛЕВАЯ РАСКЛАДКА
|
||||
|
||||
Принцип: **W3-резидент = «толстая графика, которую зовёт только главный цикл»;
|
||||
W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки =
|
||||
новая холодная логика (стражи, боёвка, будущие уровни)**.
|
||||
|
||||
```
|
||||
W1/W2 (всегда отображено) W3 резидент (стр. 0xC000) Банки W3
|
||||
────────────────────────── ───────────────────────── ─────────
|
||||
libc + libbgi pop_bg (отрисовка тайлов) guards.c
|
||||
pop_kid (интерпретатор+рисование) pop_trob (анимации тайлов) fight.c
|
||||
pop_map (коллизия/физика/предметы) pop_loose.c (loose+потолок) debug/roomnav
|
||||
pop_geom (общая геометрия/тайлы) enter_room-часть roomtest?
|
||||
pop_ctrl (ввод/диспетчер)
|
||||
roomtest (главный цикл)
|
||||
```
|
||||
|
||||
Почему так:
|
||||
|
||||
- **`pop_map` остаётся в W1/W2** — его зовут и главный цикл, и (в будущем)
|
||||
банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо
|
||||
вынести графическую часть (loose/потолок) — она уезжает в W3 к `pop_bg`
|
||||
(§5.3). После выноса `pop_map` становится **чистой логикой без единого
|
||||
вызова графики** — тот самый bank-safe API.
|
||||
- **`pop_kid` остаётся в W1/W2**: `play_seq`/`kid_set_seq`/`Kid` нужны и
|
||||
стражам (у стражей ТА ЖЕ seqtbl), а `kid_draw` зовёт только libbgi (R4).
|
||||
- **`pop_trob` остаётся резидентом**: его зовёт только главный цикл, и он сам
|
||||
зовёт `pop_bg` — идеальный житель W3.
|
||||
- **Банк стражей не зовёт ничего из W3.** Рисование стражей — либо через
|
||||
libbgi напрямую (R4), либо (лучше) резидентный `guard_draw()` в W1/W2 рядом
|
||||
с `kid_draw`, а банк только считает состояние. Тот же приём мы уже
|
||||
используем для `pop_item_taken`/`pop_loose_fell`/`pop_ceil_fell`: банк
|
||||
выставляет флаг — резидент рисует.
|
||||
|
||||
---
|
||||
|
||||
## 5. ПЛАН РАБОТ
|
||||
|
||||
Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место
|
||||
следующему.
|
||||
|
||||
### Шаг 1. `kid_data.h` (3 745 Б) → EMM-страница + порт `load_frame` — **самый большой выигрыш**
|
||||
|
||||
Сейчас `kid_seqtbl` (2310) + `kid_frames` (1205) + `kid_seq_off` (230) лежат в
|
||||
`_CODE` окна W1/W2 — это 15 % всего дефицитного пространства.
|
||||
|
||||
Как переносить:
|
||||
|
||||
1. `pop_extract_kid_data.py` дополнительно пишет `kid_data.bin` (те же три
|
||||
таблицы подряд, фиксированные смещения).
|
||||
2. Грузим её в отдельную EMM-страницу тем же способом, что уровень
|
||||
(`pop_level_load` — готовый образец), хэндл держим в `pop_kid`.
|
||||
3. **Порт `load_frame` (seg006) и глобала `cur_frame`** — в оригинале ровно
|
||||
так и сделано: раз за тик кадр копируется в структуру, а весь остальной код
|
||||
читает `cur_frame`, а не таблицу. У нас `kid_cur_dx()/kid_cur_flags()`
|
||||
станут чтением из `cur_frame` (5 байт в `_DATA`).
|
||||
4. `play_seq` оборачивается в один `gfx_w0_map(kid_data_page)` … `unmap` на
|
||||
вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).
|
||||
|
||||
Выигрыш: **−3 745 Б из W1/W2**, цена — один маппинг страницы за тик и 5 байт
|
||||
`_DATA`. Дополнительный бонус: `load_frame`/`cur_frame` — шаг К СХОДСТВУ с
|
||||
оригиналом, а не отход от него.
|
||||
|
||||
Риск: сломать `play_seq` (сердце анимации). Проверка: прогон по комнатам с
|
||||
эталонными позами (вис, подтягивание, прыжки, подъём меча).
|
||||
|
||||
### Шаг 2. Модуль `pop_geom.c` — дедуп + bank-safe фундамент
|
||||
|
||||
Сейчас продублировано между модулями:
|
||||
|
||||
| что | где | сколько |
|
||||
|---|---|---|
|
||||
| `y_to_row`/`y_to_row_mod4` | pop_bg + pop_map | 2 копии |
|
||||
| `char_dx_forward` | pop_kid + pop_map | 2 копии |
|
||||
| `x_bump[20]` | pop_kid (uint8) + pop_map (int16) | 20 + 40 Б, РАЗНЫЕ типы |
|
||||
| `y_land[5]` | pop_kid + pop_map | 10 + 10 Б |
|
||||
| `tile_is_floor` | pop_map (+ проверка кодов в roomtest) | 2 места |
|
||||
| 32-битный LCG `prandom` | pop_bg (`prandom`) + pop_trob (`trob_prandom`) | 2 копии по ~60 Б + 2 сида |
|
||||
|
||||
Собрать в один W1/W2-модуль `pop_geom.c`: таблицы `x_bump/y_land/dir_front/
|
||||
dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
|
||||
`tile_is_floor`, `prandom`. Выигрыш прямой — сотни байт (оценка 250–400 Б),
|
||||
но главное — **это и есть тот «чистый» API, который потом сможет звать банк**
|
||||
(R2): вся геометрия оказывается в W1/W2 по определению.
|
||||
|
||||
Осторожно: `prandom` у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными
|
||||
сидами (стены vs фазы факелов). Объединять функцию можно, **сиды — нет**:
|
||||
передавать сид указателем/по индексу, иначе поедет раскладка кладки.
|
||||
|
||||
### Шаг 3. Вынести loose/потолок из `pop_map` в W3
|
||||
|
||||
`pop_map` — единственный модуль W1/W2, который зовёт графику, и делает это
|
||||
ровно в одном логическом блоке: `pop_loose_tick` + `check_press` + `do_knock` +
|
||||
`fell_on_your_head` + `check_loose_fall_on_kid` + плита-потолок (~1.4–2 КБ).
|
||||
|
||||
Вынести их в `pop_loose.c`, собираемый `--w3` рядом с `pop_bg`/`pop_trob`.
|
||||
Тогда:
|
||||
|
||||
- `pop_map` = чистая логика (bank-safe, R2 соблюдён);
|
||||
- W1/W2 худеет ещё на ~1.5–2 КБ;
|
||||
- W3 растёт на столько же — а место там появится после шага 4.
|
||||
|
||||
### Шаг 4. Перебалансировка резидента W3
|
||||
|
||||
После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:
|
||||
|
||||
1. **`wall_pattern` (944 Б) + `mob_render`/`mob_tick_one` (1381 Б)** — кандидаты
|
||||
на переезд в W1/W2: их зовёт только `pop_bg`/`pop_loose`, но сами они уже
|
||||
пользуются только libbgi (R4), значит из W1/W2 работают и остаются
|
||||
достижимыми из банка.
|
||||
2. `tile_table` и мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в
|
||||
EMM-страницу **уровня** (там ~13.8 КБ свободно) — но только если чтение
|
||||
происходит под уже замапленной страницей. Сейчас `draw_tile` читает
|
||||
`tile_table` ВНЕ W0-контекста → потребуется явный маппинг на тайл. **Не
|
||||
делать раньше замера**: 30 тайлов на входе в комнату × map/unmap — терпимо,
|
||||
а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.
|
||||
3. Если и этого мало — `enter_room` (~2.1 КБ, зовётся только при смене комнаты)
|
||||
переносится в резидент W3 или в БАНК (он вызывается из главного цикла =
|
||||
резидента, значит банк допустим по R3).
|
||||
|
||||
### Шаг 5. Данные
|
||||
|
||||
1. **`room_modif[24][30]` = 720 Б** (pop_trob, `_DATA`). Нужен произвольный
|
||||
доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты ×
|
||||
30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей
|
||||
комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице
|
||||
уровня, а в `_DATA` держать кэш на 2–3 комнаты (свою + левого/правого
|
||||
соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена —
|
||||
аккуратность на швах (кнопка в одной комнате открывает ворота в другой).
|
||||
**Делать последним** — это самая «тонкая» правка по семантике.
|
||||
2. **`dl1[256]`+`dl2[256]` = 512 Б** (pop_level, `_DATA` — копии LINKLOC/
|
||||
LINKMAP уровня) — читаются при нажатии кнопки
|
||||
и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы
|
||||
уровня (она и так маппится) — но проверить, что `pop_doorlink2` не зовётся
|
||||
из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б.
|
||||
3. **`_kbdraw_down[512]` 512 Б** (libc): проверено — это **байт на скан-код**
|
||||
(`libc/kbd/_kbdraw_state.c`), хотя комментарий называет его битовой картой.
|
||||
Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и в
|
||||
`kbd_raw_down`. Трогать осторожно: raw-клавиатура уже дважды была
|
||||
источником залипаний (memory `kbd_raw_fifo_drain`,
|
||||
`kbd_overrun_wipe_modifiers`) — правку сопровождать прогоном docs/kbd-games.
|
||||
4. **`__irq_vec_buf` 513 Б** (libc IM2): таблица векторов обязана быть
|
||||
выровнена и полна — не трогать.
|
||||
|
||||
### Шаг 6. Контракт банка стражей (проектируется ДО написания кода)
|
||||
|
||||
Когда дойдём до стражей:
|
||||
|
||||
- `guards.c` собирается `--bank 1=guards.c`, режим `huge` (или `big` с
|
||||
`BANKED=W1`, если W3 окажется тесен для трамплинов).
|
||||
- **Банк зовёт только:** `pop_map` (чистая логика после шага 3), `pop_kid`
|
||||
(`play_seq`, `kid_set_seq`, `cur_frame`), `pop_geom`, libc/libbgi.
|
||||
- **Банк НЕ зовёт:** `pop_bg`, `pop_trob`, `pop_loose` (резидент W3) — ни
|
||||
прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг,
|
||||
рисует резидент (идиома `pop_item_taken`).
|
||||
- Первым делом — **пробник** (`tests/` или `--bank` на пустышке): банк зовёт
|
||||
`pop_map`-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка
|
||||
W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не
|
||||
веры в неё.
|
||||
|
||||
### Шаг 7. Мелкий дедуп в `pop_bg` (после того, как появится тест страниц)
|
||||
|
||||
- Пять функций-редроев (`pop_spike_redraw`, `pop_loose_shake_draw`,
|
||||
`pop_floor_bake`, `pop_button_redraw`, `pop_leveldoor_redraw`) отличаются
|
||||
только прямоугольником heal, банком и набором тайлов — свести к одному
|
||||
параметризованному хелперу (оценка −150…250 Б).
|
||||
- `env_b/wall_b/fore_b/pot_b` — четыре одинаковых обёртки над `blit_b`
|
||||
(оставить: экономия единицы байт, читаемость дороже).
|
||||
- `overlay_mid_tile` / `fore_only_tile` / `climb_overlay_tile` / `draw_tile`
|
||||
— общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои».
|
||||
Тут экономия потенциально сотни байт, но это **та самая зона риска из §3** —
|
||||
только с пиксельным диффом до/после и по одному слою за раз.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ожидаемый итог
|
||||
|
||||
| Шаг | W1/W2 | W3 | Риск |
|
||||
|---|---|---|---|
|
||||
| 1. kid_data → EMM + load_frame | **−3 745** | — | средний (сердце анимации) |
|
||||
| 2. pop_geom (дедуп) | −250…400 | — | низкий |
|
||||
| 3. loose → W3 | −1 500…2 000 | +1 500…2 000 | низкий (перенос как есть) |
|
||||
| 4. разгрузка W3 (wall_pattern, mob) | +2 300 | −2 300 | низкий |
|
||||
| 5. данные (room_modif, LINKLOC, kbd) | −1 000…1 600 | — | средний/высокий |
|
||||
| 7. дедуп редроев pop_bg | — | −150…250 | средний |
|
||||
|
||||
Суммарно: **W1/W2 освобождается ~4.5–6 КБ**, W3 остаётся примерно в нынешнем
|
||||
объёме, но становится «правильно заполненным» — в нём только то, что банк
|
||||
никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают
|
||||
до 16 КБ на банк, не трогая резидент.
|
||||
|
||||
---
|
||||
|
||||
## 7. Как мерить и проверять (обязательно к каждому шагу)
|
||||
|
||||
1. **До/после по `.rel`** — точные размеры на модуль:
|
||||
`for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done`
|
||||
(области `_CODE`/`_W3CODE`/`_DATA`). Итоги окон печатает сам `sprinter-cc`.
|
||||
2. **Функции** — из `.lst` (метки `_name:` и адреса), скрипт в истории этой
|
||||
сессии; полезно ловить «функция распухла после рефактора».
|
||||
3. **MAME**: любой перенос кода между окнами/страницами — это класс «молча
|
||||
ломается» (`sprinter_memory_modes`). Минимум: комната 1 (loose), 12
|
||||
(вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота).
|
||||
4. **Пиксельный дифф** для правок отрисовки: заморозить кадр (`1`), сравнить
|
||||
обе страницы дабл-буфера через `vram` (см. memory `mame_mcp_bridge`) и/или
|
||||
сверить с эталонным рендером `render_room.py`.
|
||||
5. **Скорость** — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE
|
||||
(приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.
|
||||
|
||||
## 8. Ссылки
|
||||
|
||||
- `bin/sprinter-cc` — справка по `--w3`, `--bank`, `--memory`, `--memory-manual`.
|
||||
- `runtime/crt0_banked.s`, `runtime/bank.s` — трамплины и захват резидентной
|
||||
страницы W3.
|
||||
- `libbgi/common/_bgi_begin.c` / `_bgi_end.c` — механика скобки W3 (R4).
|
||||
- memory: `w3_resident_code`, `pop_banking_architecture`, `sdcc_banking`,
|
||||
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
||||
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
||||
`libc_one_function_per_module`.
|
||||
- `applications/PoP/docs/size_optimization_plan.md` — v1 (замер 2026-07-21,
|
||||
раздел §8 про скорость отрисовки актуален и не дублируется здесь).
|
||||
@@ -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, и
|
||||
цена вопроса вырастет во столько же раз.
|
||||
@@ -0,0 +1,68 @@
|
||||
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
|
||||
|
||||
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
|
||||
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
|
||||
|
||||
## Факты из SDLPoP (подтверждено чтением исходника)
|
||||
|
||||
- `Char.room` (реальная комната персонажа) ≠ `drawn_room` (отрисованная) —
|
||||
штатное состояние.
|
||||
- **Коллизия через ±140:** `xpos_in_drawn_room()` (seg004:0405) сдвигает
|
||||
xpos на `±TILE_SIZEX*SCREEN_TILECOUNTX = ±140`, когда `curr_room` колонки
|
||||
(`curr_row_coll_room[col]`) ≠ `drawn_room` (room_L/room_BL → −140,
|
||||
room_R/room_BR → +140). Т.е. коллизия строится по РЕАЛЬНЫМ тайлам соседей.
|
||||
- **Смена экрана:** `check_the_end()` (seg000:0FBD): `if (next_room!=0 &&
|
||||
next_room!=drawn_room) { drawn_room=next_room; load_room_links; redraw }`.
|
||||
`next_room` ставится в `exit_room()` (= `Char.room` ПОСЛЕ успешного
|
||||
`leave_room`). Значит drawn_room следует за Char.room, но Char.room меняется
|
||||
ТОЛЬКО при реальном пересечении шва (leave_room, seg002:0504) на «легальном»
|
||||
кадре/действии (не turn/climb/standup).
|
||||
- **Отрисовка левого соседа:** только `load_leftroom()` (col9 левого соседа в
|
||||
левую кромку); правый сосед НЕ рисуется (изометрия). Окклюзия ворот на шве —
|
||||
только левая (seg008:696).
|
||||
- **Ceiling-полоса:** `draw_room` рисует доп. ряд из `room_A` (row2, draw_main_y
|
||||
=-1). (Уже реализовано, баг #3.)
|
||||
|
||||
## Текущее состояние нашего движка (до #4)
|
||||
|
||||
`cur_room` (=drawn_room) ВСЕГДА == комната Kid. Шов подделан: Kid остаётся в
|
||||
drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol` (коллизия ±1
|
||||
кол) / `lcol_bg` (openness ворот). Уход из комнаты — `pop_leave_dir`/`enter_room`
|
||||
МГНОВЕННО при пересечении порога `char_x`. Отсюда #4: экран переключается
|
||||
раньше, чем в оригинале (Kid должен «отступить» за кромку, оставив старую
|
||||
комнату).
|
||||
|
||||
## План (инкременты, каждый проверяется в MAME)
|
||||
|
||||
### S1. Данные + рендер-смещение Kid
|
||||
- Ввести `kid_room` (реальная комната Kid) отдельно от `cur_room`(=drawn_room).
|
||||
- `kid_x_offset()` = разница комнат: kid_room == left(drawn) → лог. x Kid −140
|
||||
(рисуется за левой кромкой); right → +140; равны → 0. (порт
|
||||
xpos_in_drawn_room).
|
||||
- `kid_draw`/heal/fore используют смещение (Kid рисуется частично за кромкой).
|
||||
- Проверка: Kid у шва рисуется со сдвигом, экран не дёргается.
|
||||
|
||||
### S2. Коллизия по kid_room
|
||||
- Коллизионный контекст (`g_fg`/edges/`g_room`/modif в pop_map) следует за
|
||||
`kid_room`, а не за drawn_room. Когда kid_room≠drawn_room — грузим
|
||||
соседа как коллизионную комнату (curr_col 0..9 в кадре kid_room).
|
||||
- Отрисовка (room_fg и т.п.) остаётся по drawn_room.
|
||||
- Порт `curr_row_coll_room[]`/`xpos_in_drawn_room` можно упростить: держим
|
||||
ОДИН коллизионный room (kid_room) + существующие снапшоты кромок для ±1 кол.
|
||||
|
||||
### S3. Отложенная смена drawn_room
|
||||
- Уход (`check_leave`/`check_leave_below`): ставит `kid_room=сосед`,
|
||||
репроецирует Kid (x∓140, col∓10) — но drawn_room НЕ меняет сразу.
|
||||
- `check_the_end`-эквивалент в главном цикле: `if (kid_room != drawn_room &&
|
||||
<условие коммита>) enter_room(kid_room)`. Условие коммита — по SDLPoP:
|
||||
как только Char.room сменилась легальным leave (не bumped/turn). Для
|
||||
«bumped назад за кромку» drawn_room остаётся (симптом #4).
|
||||
- Проверка сценариев #4/#5 в MAME.
|
||||
|
||||
### S4. Полировка
|
||||
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
|
||||
|
||||
## Связанные баги (bug_list.md)
|
||||
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
|
||||
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
|
||||
колонны). Memory: `pop_seam_room_model`.
|
||||
@@ -0,0 +1,335 @@
|
||||
# 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 (лишние блиты, которых нет в оригинале).
|
||||
@@ -27,6 +27,8 @@ static const char *const pop_env_atl[POP_ENV_PAGES] = {
|
||||
};
|
||||
#define POP_WALL_ATL "pop_wall.atl"
|
||||
#define POP_FORE_ATL "pop_fore.atl"
|
||||
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */
|
||||
#define POP_PAL_POT 0x40
|
||||
#define POP_BG_PAL "pop_bg.pal"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -36,4 +36,6 @@ static const char *const kid_atl[KID_PAGES] = {
|
||||
"kid27.atl",
|
||||
};
|
||||
#define KID_PAL_FILE "kid.pal"
|
||||
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
|
||||
#define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# roomtest — правила подпроекта
|
||||
|
||||
Живой прототип порта PoP: комната 1 уровня 1 (композиция тайлов в рантайме)
|
||||
+ Kid (анимация seqtbl, управление, коллизия, падение, зацеп, fore-окклюзия,
|
||||
проваливающиеся полы). Действуют правила корневого `CLAUDE.md` и
|
||||
`applications/PoP/CLAUDE.md`.
|
||||
|
||||
**Главное правило (из `../CLAUDE.md`): `../SDLPoP/src/` — источник истины.**
|
||||
Перед реализацией ЛЮБОЙ функции и по ЛЮБОМУ вопросу «как в оригинале» —
|
||||
СНАЧАЛА прочитай соответствующий код SDLPoP и портируй по нему; не пиши
|
||||
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
|
||||
по умолчанию баг у нас.
|
||||
|
||||
## Сборка и запуск
|
||||
|
||||
```
|
||||
make # собрать roomtest.exe (упаковав ассеты через toolchain/)
|
||||
make run # exe + EXTRA_DATA на дискету + запуск MAME (см. корневой док)
|
||||
```
|
||||
|
||||
`MEMORY=small`, `--gfx 256`. Ассеты (`pop_env0..4.atl`, `pop_wall.atl`,
|
||||
`pop_fore.atl`, `kid0..27.atl`, `kid.pal`) генерируются python-скриптами
|
||||
`../toolchain/` — Makefile дёргает их сам при изменении. Данные комнаты —
|
||||
`room1_data.h` (fg=foretable code, bg=backtable modifier); данные анимации
|
||||
Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
|
||||
|
||||
## Проверка в MAME
|
||||
|
||||
Только через `toolchain/mame_interactive.py` или MCP-мост `mame-z80`
|
||||
(см. memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
|
||||
Эталон комнаты — `../toolchain/1.1-2.png`. Пересобрал HDD-образ → MAME
|
||||
ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
|
||||
|
||||
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
|
||||
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
|
||||
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
|
||||
**ESC** — выход.
|
||||
|
||||
## Модули
|
||||
|
||||
| Файл | Роль (порт SDLPoP) |
|
||||
|------|--------------------|
|
||||
| `roomtest.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), tick→draw порядок слоёв. |
|
||||
| `pop_bg.c/.h` | Отрисовка фона композицией тайлов (`seg008` draw_tile/wall_pattern), fore-окклюзия поверх Kid (`seg003` redraw_at_char), loose-полы (shake/bake/mob, `seg007/008`). |
|
||||
| `pop_kid.c/.h` | Анимация/движение Kid: интерпретатор seqtbl `play_seq` + frame_table (`seg006`). |
|
||||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||||
|
||||
## Порядок слоёв в кадре (важно для окклюзии)
|
||||
|
||||
`roomtest.c` каждый тик рисует в СКРЫТУЮ страницу: `kid_heal` (стереть
|
||||
прошлый кадр) → `kid_tick` → `pop_phys_tick` → `pop_loose_tick` (loose СЛОЙ
|
||||
ФОНА — ДО Kid, чтобы Kid был поверх плиты) → `kid_draw` → `pop_fore_over_kid`
|
||||
(передние грани тайлов поверх Kid) → `pop_room_clip_borders` (чистка бортов,
|
||||
гейт по флагу — только в кадрах падения) → флип на vsync.
|
||||
|
||||
**Дабл-буфер:** у каждой страницы своя видео-ОЗУ и теневая ОЗУ-копия; heal
|
||||
берёт чистый фон из копии ТОЙ страницы, в которую рисуем. Любой динамический
|
||||
элемент (Kid, loose-плита, падающий кусок) обязан чистить свой прошлый кадр
|
||||
на КАЖДОЙ из двух страниц — иначе остаток виден через кадр как **мерцание**.
|
||||
Это типовой источник багов «остаётся кусочек» (см. memory
|
||||
`pop_fall_debug_baseline`).
|
||||
|
||||
## Ключевые memory
|
||||
|
||||
`pop_kid_plan`, `pop_hang_state`, `pop_fore_layer`, `pop_fall_debug_baseline`,
|
||||
`accel_vertical_copy`, `kbd_raw_fifo_drain`, `pop_check_sdlpop_first`.
|
||||
@@ -5,30 +5,70 @@ PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
||||
EXAMPLE := roomtest
|
||||
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
||||
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
||||
MEMORY ?= small
|
||||
EXTRA_FLAGS ?= --gfx 256
|
||||
EXTRA_SRCS := pop_bg.c pop_kid.c pop_ctrl.c pop_map.c
|
||||
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
||||
# Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_gdraw
|
||||
# в одной странице — вызовы между ними прямые, если функция не помечена
|
||||
# __banked). Так снято главное ограничение резидента: из банка его было
|
||||
# не достать ни прямо, ни транзитивно, а банк→банк работает (трамплин
|
||||
# сохраняет страницу окна на СТЕКЕ, проверено вложенной цепочкой
|
||||
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
|
||||
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
|
||||
# sdcc_banked_call_rules.
|
||||
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c $(PROF_FLAGS)
|
||||
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||
# MAME = время фазы в растровых строках.
|
||||
PROF ?= 1
|
||||
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
|
||||
|
||||
BG_DIR := $(CURDIR)/../poc/res/bg
|
||||
KID_DIR := $(CURDIR)/../poc/res/kid
|
||||
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
|
||||
LVL_DATA := $(LVL_DIR)/res2001.bin
|
||||
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_wall.atl $(BG_DIR)/pop_fore.atl
|
||||
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \
|
||||
$(BG_DIR)/pop_pot.atl
|
||||
# ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23)
|
||||
# не рисуются (kid_draw пропускает незагруженную страницу). Палитра —
|
||||
# ЕДИНАЯ игровая (env+wall+kid).
|
||||
KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl)
|
||||
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal
|
||||
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl
|
||||
# Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его
|
||||
# делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже.
|
||||
KID_BIN := $(KID_DIR)/kid_data.bin
|
||||
GUARD_DIR := $(CURDIR)/../poc/res/guard
|
||||
GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \
|
||||
$(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl
|
||||
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA)
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(LVL_DATA)
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
||||
# чтобы корень диска не зарастал десятками .atl.
|
||||
# BG\ — фон (env/wall/fore/pot)
|
||||
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
|
||||
# GUARD\ — стражи (появятся здесь же)
|
||||
# LEVELS\ — уровни
|
||||
hdd: $(EXAMPLE).exe
|
||||
$(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \
|
||||
$(EXAMPLE).exe \
|
||||
$(foreach f,$(BG_DATA),BG:$(f)) \
|
||||
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
||||
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
||||
LEVELS:$(LVL_DATA)
|
||||
|
||||
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
||||
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
|
||||
cd $(TC) && python3 pop_pack_bg.py
|
||||
$(GUARD_DATA): $(TC)/pop_pack_guard.py
|
||||
cd $(TC) && python3 pop_pack_guard.py
|
||||
|
||||
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
||||
cd $(TC) && python3 pop_pack_kid.py
|
||||
kid_data.h: $(TC)/pop_extract_kid_data.py
|
||||
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
|
||||
cd $(TC) && python3 pop_extract_kid_data.py
|
||||
|
||||
$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h kid_data.h room1_data.h $(BG_DATA) $(KID_DATA)
|
||||
$(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)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# roomtest
|
||||
|
||||
Живой прототип порта Prince of Persia: комната 1 уровня 1 отрисовывается
|
||||
реальной композицией слоёв-тайлов, по ней бегает и прыгает Kid под
|
||||
управлением с клавиатуры. Служит стендом для переноса механик движка
|
||||
(анимация, коллизия, падение, зацеп, окклюзия, проваливающиеся полы) до их
|
||||
выделения в переиспользуемый код.
|
||||
|
||||
Правила разработки и порядок слоёв рендера — [`CLAUDE.md`](CLAUDE.md).
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```
|
||||
make # собрать roomtest.exe (ассеты упакуются автоматически)
|
||||
make run # собрать, упаковать на дискету и запустить в MAME
|
||||
```
|
||||
|
||||
Клавиши в запущенном тесте: ←/→ бег, ↓ присед, ↑+→ прыжок, Shift шаг,
|
||||
ESC выход.
|
||||
|
||||
**Отладка рисования:**
|
||||
- **SPACE** — тумблер дабл-буфера (в однобуфере баги рисования видны без
|
||||
мерцания-через-кадр).
|
||||
- **1** — заморозить кадр (`frozen`: tick/draw/flip пропускаются, картинка
|
||||
стоит), **2** — снять заморозку и продолжить. Приём для разбора
|
||||
транзиентных артефактов (тряска/падение loose, поза Kid): поймать
|
||||
проблемный кадр на `1`, спокойно рассмотреть/снять пиксели — в т.ч.
|
||||
удалённо из MAME MCP-моста, пока анимация стоит.
|
||||
|
||||
Эталон вида комнаты — `../toolchain/1.1-2.png`.
|
||||
|
||||
## Модули
|
||||
|
||||
- `roomtest.c` — главный цикл (дабл-буфер, порядок tick/draw).
|
||||
- `pop_bg.c` — фон комнаты композицией тайлов + fore-окклюзия + loose-полы.
|
||||
- `pop_kid.c` — анимация/движение Kid (интерпретатор seqtbl).
|
||||
- `pop_ctrl.c` — управление с клавиатуры (диспетчер control).
|
||||
- `pop_map.c` — коллизия с картой, физика падения, loose-полы.
|
||||
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
|
||||
(оба генерируются скриптами `../toolchain/`).
|
||||
|
||||
## Статус
|
||||
|
||||
Готово и проверено в MAME: статический фон комнаты 1; Kid — анимация,
|
||||
управление, коллизия/падение, зацеп/подтягивание/спуск, fore-окклюзия
|
||||
пола/стены над Kid. В работе: проваливающиеся полы (loose floors) — см.
|
||||
`../docs/loose_floors_plan.md`.
|
||||
@@ -0,0 +1,463 @@
|
||||
# roomtest — список известных багов (Фаза B и смежное)
|
||||
|
||||
Найдено при тестировании Фазы B (кнопки/ворота) в MAME. **НЕ решаем сейчас**
|
||||
— задача следующего этапа. Правило проекта: механику сверять с
|
||||
`applications/PoP/SDLPoP/src/` ДО кодинга.
|
||||
|
||||
Статус на момент записи: 2026-07-21. Тестовый старт — комната 6 (`START_ROOM`
|
||||
в `roomtest.c`). Кнопка room6(0,2, opener) → ворота room8(0,9), видны в левом
|
||||
шве room6.
|
||||
|
||||
---
|
||||
|
||||
## BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — **Critical**
|
||||
|
||||
**Симптом:** при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов)
|
||||
Kid оказывается на ряду **row 1** вместо row 0 — «проваливается» на этаж ниже
|
||||
(стоит в дверном проёме на неправильной высоте).
|
||||
|
||||
**Где:** `pop_map.c` `check_leave()` (порт seg002 leave_room) — боковой уход
|
||||
`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) — как именно
|
||||
пересчитываются `Char.curr_row`/`Char.y` при side-переходе (left/right).
|
||||
Для вертикального перехода мы это сделали, для бокового — нет.
|
||||
|
||||
## BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — **Critical**
|
||||
|
||||
**Симптом:** после перехода в соседнюю комнату попытка сразу вернуться
|
||||
обратно приводит к тому, что Kid снова закидывается в ту же комнату —
|
||||
переход «пинг-понгует», выйти нельзя.
|
||||
|
||||
**Где:** `pop_map.c` `check_leave()`. После перехода Kid репроецирован к
|
||||
ПРОТИВОПОЛОЖНОЙ кромке новой комнаты (`x ∓ 140`). На СЛЕДУЮЩЕМ же кадре
|
||||
передний край снова за порогом ухода в обратную сторону → `pop_leave_dir`
|
||||
взводится опять → мгновенный обратный переход. Нужен guard «только что
|
||||
перешёл» (подавить повторный leave на N кадров / пока Kid не отойдёт от
|
||||
кромки), как в оригинале (`next_room`/leave обрабатываются раз за кадр +
|
||||
Kid входит достаточно внутрь).
|
||||
|
||||
**Сверить с SDLPoP:** порядок `leave_room`/`goto_other_room` + на какой
|
||||
X ставится персонаж при входе (должен быть НЕ на самой кромке, иначе
|
||||
мгновенный обратный триггер).
|
||||
|
||||
## BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия (Kid поверх кнопки) — **Critical**
|
||||
|
||||
**Симптом:** когда Kid подтягивается (climb-up) на тайл, верх которого —
|
||||
КНОПКА (opener/closer), а не обычный пол, отрисовка неверная: Kid рисуется
|
||||
ПОВЕРХ кнопки, хотя передняя грань пола/кнопки должна перекрывать нижнюю
|
||||
часть Kid во время подъёма.
|
||||
|
||||
**Где:** `pop_bg.c` `climb_overlay_tile()` — floor-overlay при подъёме
|
||||
рисуется только для floor-подобных тайлов `code ∈ {1,3,5,19}`. Кнопка
|
||||
(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`
|
||||
(нажатая кнопка → tiles_1_floor / tiles_5_stuck) — оверлей должен идти по
|
||||
ПОДСТАВЛЕННОМУ типу тайла.
|
||||
|
||||
---
|
||||
|
||||
## Оптимизации (отложено)
|
||||
|
||||
### OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22
|
||||
|
||||
**Возможность:** `pop_room_redraw_seam_left()` (pop_bg.c) на каждое изменение
|
||||
openness рисует `bar(BLACK)` по всему col0 + **полный `draw_tile(0,0)`**
|
||||
(стены, `topright`, `wall_pattern` с `prandom()` — десятки блитов). Реально
|
||||
анимируются только бары решётки — `draw_gate_back` (~9 `env_b`).
|
||||
|
||||
**Стоимость:** seam-блок (синий io_border) занимает ~30-50% кадрового периода,
|
||||
**но только пока openness меняется** — т.е. во время открытия и медленного
|
||||
авто-закрытия (~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,...)` + дорисовать статику
|
||||
тайла (0,0), задетую полосой. Ожидаемо срезает бóльшую часть 30-50%.
|
||||
**Пиксель-чувствительно** — полоса баров пересекается со статикой col0,
|
||||
обязательна выверка в MAME по кромкам.
|
||||
|
||||
**Решение (2026-07-22):** оставляем как есть — стоимость транзиентная, в бюджет
|
||||
помещаемся. Делать, если упрёмся в кадровый бюджет на сценах с воротами.
|
||||
|
||||
---
|
||||
|
||||
## Исправлено
|
||||
|
||||
- **Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука
|
||||
срезана до одного пикселя** — ИСПРАВЛЕНО 2026-07-28. Две причины:
|
||||
(а) не был портирован `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-07-22. `pop_spike_redraw`: `heal_off` уже возвращает всю печёную
|
||||
статику (база 127, пол, грани); поверх анимируются только два острия
|
||||
(`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
|
||||
149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки».
|
||||
Фикс: `fore_tile` применяет ту же подстановку нажатой кнопки, что и
|
||||
`draw_tile` (opener→floor / closer→stuck при таймере связи >1). Плюс
|
||||
`pop_button_redraw` — wipe своей ячейки + правой грани (дальний угол в
|
||||
0,3), низ строго yb+64 (не залезать в стену ряда 1).
|
||||
|
||||
## НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)
|
||||
|
||||
- **Голова стоящего Кида поверх падающей на него loose-плиты.** Комната 12:
|
||||
зацеп не удался, Кид остался стоять, сбитая плита падает прямо на него —
|
||||
голова рисуется ПОВЕРХ плиты. Сверено покадрово с SDLPoP v1.24 (2026-07-29):
|
||||
там ровно то же самое. Артефакт оригинального движка (порядок midtable), а
|
||||
не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев,
|
||||
которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и
|
||||
падение вместе с плитой — там плита обязана быть поверх Кида.
|
||||
|
||||
## Заметки
|
||||
|
||||
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
|
||||
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
|
||||
сторону = разворот (как в оригинале safe_step), движение со второго.
|
||||
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`).
|
||||
- `START_ROOM=6` — временный тестовый старт; вернуть на
|
||||
`pop_level_start_room()` после отладки объектов.
|
||||
|
||||
---
|
||||
|
||||
## BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **Medium**
|
||||
|
||||
**Симптом (2026-07-22):** при прыжке вверх (SEQ up, кадры 67..79) руки/голова
|
||||
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)
|
||||
|
||||
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
|
||||
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
|
||||
кадр), а у нас каждая такая пометка превращается в реальный heal (копию из
|
||||
ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем
|
||||
безусловно» корректен, но дорог.
|
||||
|
||||
## T-1. Пики: перерисовывать по причине, а не безусловно
|
||||
|
||||
**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
|
||||
каждой живой пики в комнате (порт `redraw_21h`, который `animate_spike`
|
||||
вызывает вне всяких `if`). Это корректно, но лишнее для пик, до которых
|
||||
Киду дела нет.
|
||||
|
||||
**Надо:** перерисовывать тайл пики, только если
|
||||
1. **сменился её видимый кадр** (шаг выдвижения/уборки), ЛИБО
|
||||
2. **её кто-то стёр** — а стереть у нас может только heal, то есть тайл
|
||||
попал в прямоугольник `kid_heal` этого кадра.
|
||||
|
||||
Это и есть модель оригинала, просто выраженная флагами: `redraw_at_char`
|
||||
(seg003:0576) каждый кадр помечает `set_redraw_fore` тайлы персонажа, причём
|
||||
**объединение текущего и предыдущего** прямоугольника
|
||||
(`MIN(char_top_row, prev_char_top_row)` и т.д.), а `animate_spike` помечает
|
||||
свой тайл. Итог = {тайл сменил кадр} ∪ {тайлы Кида}.
|
||||
|
||||
**Как:** слой Кида и так считает `cL..cR`/`rT..rB` в `pop_fore_over_kid` —
|
||||
пусть публикует их (плюс предыдущие, как в оригинале), а цикл trob'ов
|
||||
сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений
|
||||
прямоугольников (см. память `manual_hints_over_auto_detect`).
|
||||
|
||||
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит
|
||||
|
||||
**Сейчас:** `kid_heal` → `kid_draw` → `pop_fore_over_kid` идут каждый кадр,
|
||||
даже когда Kid стоит и в его тайлах ничего не меняется. Это ровно поведение
|
||||
оригинала (`draw_game_frame`, seg000:917 — `draw_moving()` + `draw_tables()`
|
||||
безусловно), но у него это дёшево, а у нас нет.
|
||||
|
||||
**Надо:** пропускать heal+draw Кида, когда кадр/поза/координаты не менялись
|
||||
и в его тайлах нет активной анимации.
|
||||
|
||||
**Осторожно (дабл-буфер):** пропускать можно **не раньше второго подряд**
|
||||
неизменного кадра — иначе одна из двух страниц останется со старым
|
||||
содержимым. Условие «обе страницы уже получили это состояние».
|
||||
|
||||
**Связь с T-1:** после T-1 idle-skip не требует ничего дополнительного для
|
||||
пик: раз Кида не перерисовываем — heal'а нет — стирать пики нечем — редрой
|
||||
не нужен, он отпадает сам.
|
||||
|
||||
---
|
||||
|
||||
# Обход всех 24 комнат уровня 1 — поиск багов отрисовки
|
||||
|
||||
Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо
|
||||
`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой),
|
||||
Kid ставится на первый пол, `pop_trob_reset()` возвращает пики/ворота в
|
||||
исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа
|
||||
единицы (`||` `||||` = 24). Убирается закомментированием одной строки.
|
||||
|
||||
|
||||
**Связность уровня 1 (проверено по `res2001.bin`, links @1952).** Обход графа
|
||||
от стартовой комнаты 1 показывает: комнаты **13, 18, 24 НЕДОСТИЖИМЫ** в
|
||||
обычной игре. У всех трёх ссылки наружу есть, а на них не ссылается никто
|
||||
(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 возможен «мусор в шве» — наш рендер кромки
|
||||
читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
|
||||
считает. Это свойство данных, а не наш баг. Приоритет багов в этих трёх
|
||||
комнатах — низкий (в игре их не видно).
|
||||
|
||||
| комната | статус | что не так |
|
||||
|---------|--------|------------|
|
||||
| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты |
|
||||
| 2 | | |
|
||||
| 3 | | |
|
||||
| 4 | | |
|
||||
| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` |
|
||||
| 6 | | |
|
||||
| 7 | | |
|
||||
| 8 | | |
|
||||
| 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 | | |
|
||||
| 11 | | |
|
||||
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» |
|
||||
| 13 | недостижима в игре | |
|
||||
| 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` |
|
||||
| 16 | | |
|
||||
| 17 | | |
|
||||
| 18 | недостижима в игре | |
|
||||
| 19 | | |
|
||||
| 20 | | |
|
||||
| 21 | | |
|
||||
| 22 | | |
|
||||
| 23 | | |
|
||||
| 24 | недостижима в игре | |
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* guards.c — ЛОГИКА стража, собирается в БАНК (--bank 1=guards.c, окно W3).
|
||||
*
|
||||
* Правила банка (проверено пробником tests/w3bankgfx):
|
||||
* - состояние держим в W1/W2 (pop_guard.c): статики банка линкуются в
|
||||
* страницу банка и снаружи не читаются;
|
||||
* - звать можно W1/W2-код (pop_map, pop_kid, pop_geom, libc, примитивы
|
||||
* libbgi) — скобка _bgi_begin/_bgi_end возвращает страницу БАНКА;
|
||||
* - звать НЕЛЬЗЯ резидент W3 (pop_bg) — ни напрямую, ни транзитивно:
|
||||
* пока исполняется банк, резидентной страницы в адресном пространстве
|
||||
* нет. Поэтому фон/окклюзию за стража рисует главный цикл.
|
||||
*
|
||||
* Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000
|
||||
* play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой:
|
||||
* ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а
|
||||
* дальше работает общий pop_control().
|
||||
*/
|
||||
#include "pop_guard.h"
|
||||
#include "pop_kid.h"
|
||||
#include "pop_ctrl.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_geom.h"
|
||||
#include "pop_trob.h"
|
||||
#include "pop_state.h" /* pop_immortal — чит бессмертия */
|
||||
|
||||
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
|
||||
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
|
||||
|
||||
#define TILE_GATE 4
|
||||
#define TILE_DOORTOP_FLOOR 7
|
||||
#define TILE_LOOSE 11
|
||||
#define TILE_DOORTOP 12
|
||||
#define TILE_CHOMPER 18
|
||||
#define TILE_WALL 20
|
||||
|
||||
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
|
||||
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его
|
||||
* через границу банка дороже, чем повторить. */
|
||||
static uint8_t tile_is_floor(uint8_t t)
|
||||
{
|
||||
switch (t) {
|
||||
case 0: case 9: case 12: case 20:
|
||||
case 26: case 27: case 28: case 29:
|
||||
return 0;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid,
|
||||
* seg003:761): xpos − 7 делится на ширину тайла с округлением вниз. */
|
||||
static int8_t tile_col; /* колонка последнего tile_at_kid */
|
||||
|
||||
static uint8_t tile_at_kid(int16_t xpos)
|
||||
{
|
||||
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos − 7 − 58)/14),
|
||||
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]).
|
||||
* Оригинал берёт её из tile_div_tbl, у нас — делением с округлением
|
||||
* вниз, как в pop_map. */
|
||||
int16_t x = xpos - 7 - SCREENSPACE_X;
|
||||
int16_t col = x / TILE_SIZEX;
|
||||
if (x % TILE_SIZEX < 0) --col;
|
||||
tile_col = (int8_t)col;
|
||||
return pop_tile_at(tile_col, Kid.curr_row);
|
||||
}
|
||||
|
||||
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
|
||||
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
|
||||
* 2 видит и пойдёт. */
|
||||
void pop_check_can_guard_see_kid(void) __banked
|
||||
{
|
||||
uint8_t kid_frame = Kid.frame;
|
||||
int16_t left_pos, right_pos;
|
||||
uint8_t tile;
|
||||
|
||||
if (!(Guard.charid != 0 &&
|
||||
/* кадры 219..228 — подъём по лестнице: там страж Кида не видит */
|
||||
kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) &&
|
||||
Kid.alive < 0 && Guard.alive < 0 &&
|
||||
Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) {
|
||||
can_guard_see_kid = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
can_guard_see_kid = 2;
|
||||
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||
if (left_pos > right_pos) {
|
||||
int16_t t = left_pos; left_pos = right_pos; right_pos = t;
|
||||
}
|
||||
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
|
||||
if (tile_at_kid(left_pos) == TILE_CHOMPER)
|
||||
left_pos += TILE_SIZEX;
|
||||
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
|
||||
if (tile_at_kid(right_pos) == TILE_GATE)
|
||||
right_pos -= TILE_SIZEX;
|
||||
|
||||
while (left_pos <= right_pos) {
|
||||
tile = tile_at_kid(left_pos);
|
||||
/* Сквозь это не видно вовсе. */
|
||||
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
|
||||
can_guard_see_kid = 0;
|
||||
return;
|
||||
}
|
||||
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
|
||||
* упрётся в неоткрытые ворота / шагнёт в дыру. */
|
||||
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
|
||||
can_guard_see_kid = 1;
|
||||
} else if (tile == TILE_GATE &&
|
||||
pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) {
|
||||
can_guard_see_kid = 1; /* ворота подняты не до конца */
|
||||
}
|
||||
left_pos += TILE_SIZEX;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
|
||||
static void move_0_nothing(void)
|
||||
{
|
||||
control_shift = control_y = control_x = control_shift2 = 0;
|
||||
control_down = control_up = control_backward = control_forward = 0;
|
||||
}
|
||||
static void move_1_forward(void)
|
||||
{
|
||||
control_x = POP_CONTROL_HELD_FORWARD;
|
||||
control_forward = POP_CONTROL_HELD;
|
||||
}
|
||||
static void move_2_backward(void)
|
||||
{
|
||||
control_backward = POP_CONTROL_HELD;
|
||||
control_x = POP_CONTROL_HELD_BACKWARD;
|
||||
}
|
||||
static void move_3_up(void)
|
||||
{
|
||||
control_y = POP_CONTROL_HELD_UP;
|
||||
control_up = POP_CONTROL_HELD;
|
||||
}
|
||||
static void move_4_down(void)
|
||||
{
|
||||
control_down = POP_CONTROL_HELD;
|
||||
control_y = POP_CONTROL_HELD_DOWN;
|
||||
}
|
||||
static void move_6_shift(void)
|
||||
{
|
||||
control_shift = POP_CONTROL_HELD;
|
||||
control_shift2 = POP_CONTROL_HELD;
|
||||
}
|
||||
static void move_down_back(void)
|
||||
{
|
||||
control_down = POP_CONTROL_HELD;
|
||||
move_2_backward();
|
||||
}
|
||||
static void move_down_forw(void)
|
||||
{
|
||||
control_down = POP_CONTROL_HELD;
|
||||
move_1_forward();
|
||||
}
|
||||
|
||||
/* autocontrol_guard_inactive (seg002:710): страж с убранным мечом.
|
||||
* Пока Кид за спиной — страж не реагирует; исключение — «шум» Кида
|
||||
* (is_guard_notice), от которого страж оборачивается. */
|
||||
static void autocontrol_guard_inactive(void)
|
||||
{
|
||||
int16_t distance;
|
||||
if (Kid.alive >= 0) return;
|
||||
distance = pop_char_opp_dist();
|
||||
/* В оригинале условие записано как (word)distance < (word)-8 —
|
||||
* беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона −8..−1.
|
||||
* То есть: Кид не «вплотную за спиной». */
|
||||
if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) {
|
||||
if (is_guard_notice) {
|
||||
is_guard_notice = 0;
|
||||
if (distance < 0) {
|
||||
if (distance < -4) move_4_down(); /* обернуться на шум */
|
||||
return;
|
||||
}
|
||||
} else if (distance < 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (can_guard_see_kid) {
|
||||
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
|
||||
move_down_forw();
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Активный страж (меч обнажён), seg002:737.. -------------------- *
|
||||
* Таблицы вероятностей по 12 градациям мастерства (seg002:26..38). Бросок
|
||||
* — prandom(255) с СОБСТВЕННЫМ сидом: у нас последовательности разведены
|
||||
* по доменам (кладка / анимация тайлов / бой), см. pop_geom.h. */
|
||||
static const uint8_t STRIKEPROB[NUM_GUARD_SKILLS] = { 61,100, 61, 61, 61, 40,100,220, 0, 48, 32, 48};
|
||||
static const uint8_t RESTRIKEPROB[NUM_GUARD_SKILLS] = { 0, 0, 0, 5, 5,175, 16, 8, 0,255,255,150};
|
||||
static const uint8_t BLOCKPROB[NUM_GUARD_SKILLS] = { 0,150,150,200,200,255,200,250, 0,255,255,255};
|
||||
static const uint8_t IMPBLOCKPROB[NUM_GUARD_SKILLS] = { 0, 61, 61,100,100,145,100,250, 0,145,255,175};
|
||||
static const uint8_t ADVPROB[NUM_GUARD_SKILLS] = {255,200,200,200,255,255,200, 0, 0,255,100,100};
|
||||
|
||||
/* Бросок «сработало ли»: в оригинале prob > prandom(255). */
|
||||
static uint8_t roll(uint8_t prob)
|
||||
{
|
||||
return (uint8_t)(prob > (uint8_t)pop_prandom(&pop_fight_seed, 255));
|
||||
}
|
||||
|
||||
/* guard_advance (seg002:0AF5): наступать. Мастерство 0 наступает всегда,
|
||||
* остальные — только пока Кид не замахнулся. */
|
||||
static void guard_advance(void)
|
||||
{
|
||||
if (guard_skill == 0 || kid_sword_strike == 0) {
|
||||
if (roll(ADVPROB[guard_skill])) move_1_forward();
|
||||
}
|
||||
}
|
||||
|
||||
/* guard_block (seg002:0B1D): блокировать замах Кида. Если Кид только что
|
||||
* пробил блок (justblocked) — своя, «импровизационная» вероятность. */
|
||||
static void guard_block(void)
|
||||
{
|
||||
uint8_t of = Opp.frame;
|
||||
if (of == FRAME_152_STRIKE_2 || of == FRAME_153_STRIKE_3 ||
|
||||
of == FRAME_162_BLOCK_TO_STRIKE) {
|
||||
if (roll(justblocked ? IMPBLOCKPROB[guard_skill]
|
||||
: BLOCKPROB[guard_skill]))
|
||||
move_3_up();
|
||||
}
|
||||
}
|
||||
|
||||
/* guard_strike (seg002:0B73): бить. По замаху Кида не бьём (кадры 169/151);
|
||||
* из своего парирования — по отдельной вероятности «добить». */
|
||||
static void guard_strike(void)
|
||||
{
|
||||
uint8_t of = Opp.frame, cf = Char.frame;
|
||||
if (of == FRAME_169_BEGIN_BLOCK || of == FRAME_151_STRIKE_1) return;
|
||||
if (cf == FRAME_161_PARRY || cf == FRAME_150_PARRY) {
|
||||
if (roll(RESTRIKEPROB[guard_skill])) move_6_shift();
|
||||
} else {
|
||||
if (roll(STRIKEPROB[guard_skill])) move_6_shift();
|
||||
}
|
||||
}
|
||||
|
||||
/* autocontrol_guard_kid_armed (seg002:0AC1): Кид с мечом. Вплотную и
|
||||
* далеко — наступаем; в «рабочей» дистанции 10..28 сначала пробуем блок, и
|
||||
* только отдышавшись бьём. */
|
||||
static void autocontrol_guard_kid_armed(int16_t distance)
|
||||
{
|
||||
if (distance < 10 || distance >= 29) {
|
||||
guard_advance();
|
||||
} else {
|
||||
guard_block();
|
||||
if (guard_refrac == 0) {
|
||||
if (distance < 12 || distance >= 29) guard_advance();
|
||||
else guard_strike();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* autocontrol_guard_kid_in_sight (seg002:0A93). */
|
||||
static void autocontrol_guard_kid_in_sight(int16_t distance)
|
||||
{
|
||||
if (Opp.sword == SWORD_2_DRAWN) {
|
||||
autocontrol_guard_kid_armed(distance);
|
||||
} else if (guard_refrac == 0) {
|
||||
/* Кид безоружен: подойти и рубить. */
|
||||
if (distance < 29) move_6_shift();
|
||||
else move_1_forward();
|
||||
}
|
||||
}
|
||||
|
||||
/* Тайл перед стражем / через один (порт get_tile_infrontof_char и
|
||||
* get_tile_infrontof2_char, seg004) — но для Char, а не для Kid. */
|
||||
static uint8_t tile_infront(int8_t step)
|
||||
{
|
||||
int8_t d = (int8_t)(Char.direction < 0 ? -step : step);
|
||||
return pop_tile_at((int8_t)(Char.curr_col + d), Char.curr_row);
|
||||
}
|
||||
|
||||
/* autocontrol_guard_kid_far (seg002:09CB): идём, если впереди есть пол хотя
|
||||
* бы через тайл; иначе пятимся (не шагать в пропасть). */
|
||||
static void autocontrol_guard_kid_far(void)
|
||||
{
|
||||
if (tile_is_floor(tile_infront(1)) || tile_is_floor(tile_infront(2)))
|
||||
move_1_forward();
|
||||
else
|
||||
move_2_backward();
|
||||
}
|
||||
|
||||
/* autocontrol_guard_active (seg002:737). */
|
||||
static void autocontrol_guard_active(void)
|
||||
{
|
||||
uint8_t cf = Char.frame;
|
||||
uint8_t of;
|
||||
int16_t distance;
|
||||
|
||||
/* Кадр 166 (неактивная стойка) и всё, что ниже 150, — не боевые позы. */
|
||||
if (cf == FRAME_166_STAND_INACTIVE || cf < 150 || can_guard_see_kid == 1)
|
||||
return;
|
||||
|
||||
if (can_guard_see_kid == 0) {
|
||||
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе
|
||||
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */
|
||||
if (Char.charid != CHARID_4_SKELETON) move_down_back();
|
||||
return;
|
||||
}
|
||||
|
||||
/* can_guard_see_kid == 2 */
|
||||
of = Opp.frame;
|
||||
distance = pop_char_opp_dist();
|
||||
/* Кид падает/приземляется в отскоке — не мешаем ему, ждём. */
|
||||
if (distance >= 12 && of >= 102 && of < 118 && Opp.action == 5)
|
||||
return;
|
||||
|
||||
if (distance < 35) {
|
||||
if ((Char.sword < SWORD_2_DRAWN && distance < 8) || distance < 12) {
|
||||
/* Слишком близко: если смотрим в одну сторону — развернуться. */
|
||||
if (Char.direction == Opp.direction) move_2_backward();
|
||||
else move_1_forward();
|
||||
} else {
|
||||
autocontrol_guard_kid_in_sight(distance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (guard_refrac != 0) return;
|
||||
if (Char.direction != Opp.direction) {
|
||||
/* Кид разбежался — встречаем ударом, а не шагом. */
|
||||
if (of >= 7 && of < 15) { /* бег */
|
||||
if (distance < 40) move_6_shift();
|
||||
return;
|
||||
} else if (of >= 34 && of < 44) { /* разбег в прыжок */
|
||||
if (distance < 50) move_6_shift();
|
||||
return;
|
||||
}
|
||||
}
|
||||
autocontrol_guard_kid_far();
|
||||
}
|
||||
|
||||
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
|
||||
static void autocontrol_opponent(void)
|
||||
{
|
||||
move_0_nothing();
|
||||
/* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */
|
||||
if (justblocked) --justblocked;
|
||||
if (kid_sword_strike) --kid_sword_strike;
|
||||
if (guard_refrac) --guard_refrac;
|
||||
if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
|
||||
else autocontrol_guard_active();
|
||||
}
|
||||
|
||||
/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */
|
||||
#define ACTION_99_HURT 99
|
||||
#define ACTION_1_RUN 1
|
||||
#define FRAME_154_POKING 154
|
||||
#define SEQ_69_ATTACK_WAS_PARRIED 69
|
||||
#define SEQ_74_HIT_BY_SWORD 74
|
||||
#define SEQ_85_STABBED_TO_DEATH 85
|
||||
|
||||
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
|
||||
static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
|
||||
{ 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0};
|
||||
|
||||
/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту.
|
||||
* Возврат 1 — HP кончились. Чит бессмертия ловим здесь: анимацию удара
|
||||
* Кид получает, а HP не теряет. */
|
||||
static uint8_t take_hp(uint8_t count)
|
||||
{
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
if (pop_immortal) return 0;
|
||||
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
|
||||
hitp_delta = (int8_t)-(int8_t)count;
|
||||
} else {
|
||||
if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
|
||||
guardhp_delta = (int8_t)-(int8_t)count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
|
||||
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
|
||||
* «получил удар».
|
||||
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
|
||||
* меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map
|
||||
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
|
||||
* идёт ровно нашей веткой. */
|
||||
static void hurt_by_sword(void)
|
||||
{
|
||||
if (Char.alive >= 0) return;
|
||||
/* Чит бессмертия перекрывает и «безоружного закалывают насмерть»:
|
||||
* без этого Кид гибнет с одного удара мимо стойки, и чит бесполезен
|
||||
* ровно там, где нужен. Анимацию удара оставляем. */
|
||||
if (Char.charid == CHARID_0_KID && pop_immortal) {
|
||||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
play_seq();
|
||||
return;
|
||||
}
|
||||
if (Char.sword != SWORD_2_DRAWN) {
|
||||
take_hp(100);
|
||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
|
||||
} else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) {
|
||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
|
||||
} else {
|
||||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||
}
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
play_seq(); /* TODO: звук 12/13 */
|
||||
}
|
||||
|
||||
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
|
||||
* Бьют кадры 153/154; если соперник в этот момент парирует — удар отбит. */
|
||||
static void check_hurting(void)
|
||||
{
|
||||
int16_t distance;
|
||||
uint8_t cf, of, min_range;
|
||||
|
||||
if (Char.sword != SWORD_2_DRAWN) return;
|
||||
if (Char.curr_row != Opp.curr_row) return;
|
||||
cf = Char.frame;
|
||||
if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return;
|
||||
|
||||
distance = pop_char_opp_dist();
|
||||
of = Opp.frame;
|
||||
if (distance < 0 || distance >= 29 ||
|
||||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
||||
/* Соперник НЕ парирует. */
|
||||
if (cf == FRAME_154_POKING) {
|
||||
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
||||
distance = pop_char_opp_dist();
|
||||
if (distance >= (int16_t)min_range && distance < 29)
|
||||
Opp.action = ACTION_99_HURT;
|
||||
}
|
||||
} else {
|
||||
/* Парировано. */
|
||||
Opp.frame = FRAME_161_PARRY;
|
||||
if (Char.charid != CHARID_0_KID) justblocked = 4;
|
||||
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
|
||||
play_seq();
|
||||
}
|
||||
}
|
||||
|
||||
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
|
||||
void pop_check_sword_hurting(void) __banked
|
||||
{
|
||||
uint8_t kf = Kid.frame;
|
||||
if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */
|
||||
pop_loadshad_and_opp();
|
||||
check_hurting();
|
||||
pop_saveshad_and_opp();
|
||||
pop_loadkid_and_opp();
|
||||
check_hurting();
|
||||
pop_savekid_and_opp();
|
||||
}
|
||||
|
||||
/* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если
|
||||
* ранен страж — Киду в этом кадре урон уже не проходит (в оригинале так же:
|
||||
* его action сбрасывается ДО проверки). */
|
||||
void pop_check_sword_hurt(void) __banked
|
||||
{
|
||||
if (Guard.action == ACTION_99_HURT) {
|
||||
if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN;
|
||||
pop_loadshad();
|
||||
hurt_by_sword();
|
||||
pop_saveshad();
|
||||
guard_refrac = (int8_t)REFRACTIMER[guard_skill];
|
||||
} else if (Kid.action == ACTION_99_HURT) {
|
||||
pop_loadkid();
|
||||
hurt_by_sword();
|
||||
pop_savekid();
|
||||
}
|
||||
}
|
||||
|
||||
/* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»:
|
||||
* пока страж жив (alive < 0) и HP есть — обычный ход; как только HP дошли до
|
||||
* нуля, он переводится в «умирает» (alive = 0), и дальше тот же control()
|
||||
* ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто
|
||||
* доигрывает последовательность. */
|
||||
static void play_guard(void)
|
||||
{
|
||||
if (Char.alive < 0 && guardhp_curr == 0) {
|
||||
Char.alive = 0;
|
||||
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
|
||||
* которого нет; для уровня 1 других эффектов там нет. */
|
||||
}
|
||||
autocontrol_opponent();
|
||||
pop_control();
|
||||
}
|
||||
|
||||
/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у
|
||||
* нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора
|
||||
* последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) —
|
||||
* отдельным шагом. */
|
||||
void pop_guard_tick(void) __banked
|
||||
{
|
||||
if (Guard.charid == 0) return; /* стража в комнате нет */
|
||||
pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */
|
||||
play_guard();
|
||||
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
|
||||
* выбирается по Char.charid внутри load_frame (seg006:0293). */
|
||||
play_seq();
|
||||
pop_saveshad();
|
||||
}
|
||||
@@ -8,449 +8,20 @@
|
||||
|
||||
#define SEQTBL_BASE 0x196Eu
|
||||
|
||||
/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */
|
||||
/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid).
|
||||
* САМА таблица кадров и seqtbl лежат в kid_data.bin (EMM-
|
||||
* страница); здесь только тип и размеры. */
|
||||
typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe;
|
||||
#define KID_NFRAMES 241
|
||||
static const kframe kid_frames[KID_NFRAMES] = {
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{ 0, 1, 0,0xC4,0x00},
|
||||
{ 1, 1, 0,0x44,0x00},
|
||||
{ 2, 3, 0,0x47,0x00},
|
||||
{ 3, 4, 0,0x48,0x00},
|
||||
{ 4, 0, 0,0xE6,0x00},
|
||||
{ 5, 0, 0,0x49,0x00},
|
||||
{ 6, 0, 0,0x4A,0x00},
|
||||
{ 7, 0, 0,0xC5,0x00},
|
||||
{ 8, 0, 0,0x44,0x00},
|
||||
{ 9, 0, 0,0x47,0x00},
|
||||
{ 10, 0, 0,0x4B,0x00},
|
||||
{ 11, 0, 0,0x43,0x00},
|
||||
{ 12, 0, 0,0xC3,0x00},
|
||||
{ 13, 0, 0,0x47,0x00},
|
||||
{ 14, 0, 0,0x43,0x09},
|
||||
{ 15, 0, 0,0xC3,0x00},
|
||||
{ 16, 0, 0,0x44,0x00},
|
||||
{ 17, 0, 0,0x46,0x00},
|
||||
{ 18, 0, 0,0x48,0x00},
|
||||
{ 19, 0, 0,0x89,0x00},
|
||||
{ 20, 0, 0,0x0B,0x00},
|
||||
{ 21, 0, 0,0x8B,0x00},
|
||||
{ 22, 0, 0,0x11,0x00},
|
||||
{ 23, 0, 0,0x07,0x00},
|
||||
{ 24, 0, 0,0x05,0x00},
|
||||
{ 25, 0, 0,0xC1,0x00},
|
||||
{ 26, 0, 0,0xC6,0x00},
|
||||
{ 27, 0, 0,0x43,0x00},
|
||||
{ 28, 0, 0,0x48,0x00},
|
||||
{ 29, 0, 0,0x42,0x00},
|
||||
{ 30, 0, 0,0x42,0x00},
|
||||
{ 31, 0, 0,0xC2,0x00},
|
||||
{ 32, 0, 0,0xC2,0x00},
|
||||
{ 33, 0, 0,0x43,0x00},
|
||||
{ 34, 0, 0,0x48,0x00},
|
||||
{ 35, 0, 0,0xCE,0x00},
|
||||
{ 36, 0, 0,0xC1,0x00},
|
||||
{ 37, 0, 0,0x45,0x00},
|
||||
{ 38, 0, 0,0x8E,0x00},
|
||||
{ 39, 0, 0,0x0B,0x00},
|
||||
{ 40, 0, 0,0x8B,0x00},
|
||||
{ 41, 0, 0,0x8A,0x00},
|
||||
{ 42, 0, 0,0x01,0x00},
|
||||
{ 43, 0, 0,0xC4,0x00},
|
||||
{ 44, 0, 0,0xC3,0x00},
|
||||
{ 45, 0, 0,0xC3,0x00},
|
||||
{ 46, 0, 0,0xA5,0x00},
|
||||
{ 47, 0, 0,0xA4,0x00},
|
||||
{ 48, 0, 0,0x66,0x00},
|
||||
{ 49, 4, 0,0x67,0x00},
|
||||
{ 50, 3, 0,0x66,0x00},
|
||||
{ 51, 1, 0,0x44,0x00},
|
||||
{ 64, 0, 0,0xC2,0x00},
|
||||
{ 65, 0, 0,0x41,0x00},
|
||||
{ 66, 0, 0,0x42,0x00},
|
||||
{ 67, 0, 0,0x00,0x00},
|
||||
{ 68, 0, 0,0x00,0x00},
|
||||
{ 69, 0, 0,0x80,0x00},
|
||||
{ 70, 0, 0,0x00,0x00},
|
||||
{ 71, 0, 0,0x80,0x00},
|
||||
{ 72, 0, 0,0x00,0x00},
|
||||
{ 73, 0, 0,0x80,0x00},
|
||||
{ 74, 0, 0,0x00,0x00},
|
||||
{ 75, 0, 0,0x00,0x00},
|
||||
{ 76, 0, 0,0x80,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{ 80, -2, 0,0x41,0x00},
|
||||
{ 81, -2, 0,0x41,0x00},
|
||||
{ 82, -1, 0,0xC2,0x00},
|
||||
{ 83, -2, 0,0x42,0x00},
|
||||
{ 84, -2, 0,0x41,0x00},
|
||||
{ 85, -2, 0,0x41,0x00},
|
||||
{ 86, -2, 0,0x41,0x00},
|
||||
{ 87, -1, 0,0x07,0x00},
|
||||
{ 88, -1, 0,0x05,0x00},
|
||||
{ 89, 2, 0,0x07,0x00},
|
||||
{ 90, 2, 0,0x07,0x00},
|
||||
{ 91, 2, -3,0x00,0x00},
|
||||
{ 92, 2, -10,0x00,0x00},
|
||||
{ 93, 2, -11,0x80,0x00},
|
||||
{ 94, 3, -2,0x43,0x00},
|
||||
{ 95, 3, 0,0xC3,0x00},
|
||||
{ 96, 3, 0,0xC3,0x00},
|
||||
{ 97, 3, 0,0x63,0x00},
|
||||
{ 98, 4, 0,0xE3,0x00},
|
||||
{ 28, 0, 0,0x00,0x00},
|
||||
{ 99, 7, -14,0x80,0x00},
|
||||
{100, 7, -12,0x80,0x00},
|
||||
{101, 4, -12,0x00,0x00},
|
||||
{102, 3, -10,0x80,0x00},
|
||||
{103, 2, -10,0x80,0x00},
|
||||
{104, 1, -10,0x80,0x00},
|
||||
{105, 0, -11,0x00,0x00},
|
||||
{106, -1, -12,0x00,0x00},
|
||||
{107, -1, -14,0x00,0x00},
|
||||
{108, -1, -14,0x00,0x00},
|
||||
{109, -1, -15,0x80,0x00},
|
||||
{110, -1, -15,0x80,0x00},
|
||||
{111, 0, -15,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{112, 0, 0,0xC6,0x00},
|
||||
{113, 0, 0,0x46,0x00},
|
||||
{114, 0, 0,0xC5,0x00},
|
||||
{115, 0, 0,0x45,0x00},
|
||||
{116, 0, 0,0xC2,0x00},
|
||||
{117, 0, 0,0xC4,0x00},
|
||||
{118, 0, 0,0xC5,0x00},
|
||||
{119, 0, 0,0x46,0x00},
|
||||
{120, 0, 0,0x47,0x00},
|
||||
{121, 0, 0,0x47,0x00},
|
||||
{122, 0, 0,0x49,0x00},
|
||||
{123, 0, 0,0xC8,0x00},
|
||||
{124, 0, 0,0xC9,0x00},
|
||||
{125, 0, 0,0x49,0x00},
|
||||
{126, 0, 0,0x45,0x00},
|
||||
{127, 2, 0,0x45,0x00},
|
||||
{128, 2, 0,0xC5,0x00},
|
||||
{129, 0, 0,0xC3,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{133, 0, 0,0x43,0x00},
|
||||
{134, 0, 0,0xC4,0x00},
|
||||
{135, 0, 0,0xC5,0x00},
|
||||
{136, 0, 0,0x48,0x00},
|
||||
{137, 0, 0,0x6C,0x00},
|
||||
{138, 0, 0,0xEF,0x00},
|
||||
{139, 0, 0,0x63,0x00},
|
||||
{140, 0, 0,0xC3,0x00},
|
||||
{141, 0, 0,0x43,0x00},
|
||||
{142, 0, 0,0x43,0x00},
|
||||
{143, 0, 0,0x44,0x00},
|
||||
{144, 0, 0,0x44,0x00},
|
||||
{172, 0, 1,0xC1,0x00},
|
||||
{173, 0, 1,0xC7,0x00},
|
||||
{145, 0, -12,0x01,0x00},
|
||||
{146, 0, -21,0x00,0x00},
|
||||
{147, 1, -26,0x80,0x00},
|
||||
{148, 4, -32,0x80,0x00},
|
||||
{149, 6, -36,0x81,0x00},
|
||||
{150, 7, -41,0x82,0x00},
|
||||
{151, 2, 17,0x42,0x00},
|
||||
{152, 4, 9,0xC4,0x00},
|
||||
{153, 4, 5,0xC9,0x00},
|
||||
{154, 4, 4,0xC8,0x00},
|
||||
{155, 5, 0,0x69,0x00},
|
||||
{156, 5, 0,0xE9,0x00},
|
||||
{157, 5, 0,0xE8,0x00},
|
||||
{158, 5, 0,0x69,0x00},
|
||||
{159, 5, 0,0x69,0x00},
|
||||
{184, 0, 2,0x80,0x10},
|
||||
{174, 0, 2,0x80,0x1A},
|
||||
{175, 3, 2,0x00,0x12},
|
||||
{176, 7, 2,0xC4,0x16},
|
||||
{177, 10, 2,0x00,0x15},
|
||||
{178, 7, 2,0x80,0x17},
|
||||
{179, 4, 2,0x80,0x19},
|
||||
{180, 0, 2,0xCE,0x18},
|
||||
{181, 0, 2,0xCD,0x0F},
|
||||
{182, 3, 2,0x00,0x14},
|
||||
{183, 3, 2,0x00,0x1F},
|
||||
{184, 0, 2,0x80,0x10},
|
||||
{185, 0, 2,0x80,0x11},
|
||||
{186, 0, 2,0x00,0x20},
|
||||
{187, 0, 2,0x80,0x21},
|
||||
{188, 2, 2,0xC3,0x22},
|
||||
{ 14, 0, 0,0x43,0x00},
|
||||
{189, 7, 2,0x80,0x13},
|
||||
{190, 1, 2,0x80,0x0E},
|
||||
{191, 0, 2,0x80,0x1B},
|
||||
{181, 0, 2,0xCD,0x0F},
|
||||
{181, 0, 2,0xCD,0x0F},
|
||||
{112, 0, 0,0xC6,0x2B},
|
||||
{113, 0, 0,0x46,0x2C},
|
||||
{114, 0, 0,0xC5,0x2D},
|
||||
{115, 0, 0,0x45,0x2E},
|
||||
{114, 0, 0,0xC5,0x00},
|
||||
{ 78, 0, 3,0x8A,0x00},
|
||||
{ 77, 4, 3,0x87,0x00},
|
||||
{211, 0, 1,0x44,0x00},
|
||||
{212, 0, 1,0x44,0x00},
|
||||
{213, 0, 1,0x44,0x00},
|
||||
{214, 0, 1,0x47,0x00},
|
||||
{215, 0, 7,0x4B,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{ 79, 4, 7,0x49,0x00},
|
||||
{130, 0, 0,0x44,0x00},
|
||||
{131, 0, 0,0x44,0x00},
|
||||
{132, 0, 2,0x44,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{192, 0, 0,0x00,0x00},
|
||||
{193, 0, 1,0x00,0x00},
|
||||
{194, 0, 0,0x80,0x00},
|
||||
{195, 0, 0,0x00,0x00},
|
||||
{196, -1, 0,0x00,0x00},
|
||||
{197, -1, 0,0x00,0x00},
|
||||
{198, -1, 0,0x00,0x00},
|
||||
{199, -4, 0,0x00,0x00},
|
||||
{200, -4, 0,0x80,0x00},
|
||||
{201, -4, 0,0x00,0x00},
|
||||
{202, -4, 0,0x00,0x00},
|
||||
{203, -4, 0,0x00,0x00},
|
||||
{204, -4, 0,0x00,0x00},
|
||||
{205, -5, 0,0x00,0x00},
|
||||
{206, -5, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{207, 0, 1,0x46,0x00},
|
||||
{208, 0, 1,0xC6,0x00},
|
||||
{209, 0, 1,0xC8,0x00},
|
||||
{210, 0, 1,0x4A,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{255, 0, 0,0x00,0x00},
|
||||
{ 52, 0, 0,0x80,0x00},
|
||||
{ 53, 0, 0,0x00,0x00},
|
||||
{ 54, 0, 0,0x00,0x00},
|
||||
{ 55, 0, 0,0x00,0x00},
|
||||
{ 56, 0, 0,0x80,0x00},
|
||||
{ 57, 0, 0,0x00,0x00},
|
||||
{ 58, 0, 0,0x00,0x00},
|
||||
{ 59, 0, 0,0x00,0x00},
|
||||
{ 60, 0, 0,0x80,0x00},
|
||||
{ 61, 0, 0,0x00,0x00},
|
||||
{ 62, 0, 0,0x80,0x00},
|
||||
{ 63, 0, 0,0x00,0x00},
|
||||
{160, 1, 1,0xC3,0x23},
|
||||
{161, 0, 1,0x49,0x24},
|
||||
{162, 0, 1,0xC3,0x25},
|
||||
{163, 0, 1,0x49,0x26},
|
||||
{164, 0, 1,0xC3,0x27},
|
||||
{165, 1, 1,0x49,0x28},
|
||||
{166, 1, 1,0x43,0x29},
|
||||
{167, 1, 1,0xC9,0x2A},
|
||||
{168, 4, 1,0xC6,0x00},
|
||||
{169, 3, 1,0xCA,0x00},
|
||||
{170, 1, 1,0x43,0x00},
|
||||
{171, 1, 1,0xC8,0x00},
|
||||
};
|
||||
|
||||
#define KID_SEQTBL_LEN 2310
|
||||
static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {
|
||||
0xF9,0x01,0xFF,0x81,0x19,0xF9,0x01,0x01,0x02,0x03,0x04,0xFB,
|
||||
0x08,0x05,0xFB,0x03,0x06,0xFB,0x03,0x07,0xFB,0x05,0x08,0xFB,
|
||||
0x01,0xF2,0x01,0x09,0xFB,0x02,0x0A,0xFB,0x04,0x0B,0xFB,0x05,
|
||||
0x0C,0xFB,0x02,0xF2,0x01,0x0D,0xFB,0x03,0x0E,0xFB,0x04,0xFF,
|
||||
0x81,0x19,0xF9,0x00,0x0F,0xFF,0xA0,0x19,0xF9,0x01,0xA6,0xFF,
|
||||
0xA8,0x19,0xF9,0x05,0xFB,0x0A,0xB1,0xB1,0xFB,0xF9,0xFA,0xFE,
|
||||
0xB2,0xFB,0x05,0xFA,0x02,0xA6,0xFB,0xFF,0xFF,0xD2,0x19,0xFF,
|
||||
0xD2,0x19,0xF9,0x01,0xFB,0x02,0xCF,0xD0,0xFB,0x02,0xD1,0xFB,
|
||||
0x02,0xD2,0xFB,0x03,0xF9,0x01,0xF2,0x00,0x9E,0xAA,0xAB,0xFF,
|
||||
0xD8,0x19,0xF9,0x05,0xF8,0xFF,0x00,0xAC,0xFB,0xFF,0xFA,0x01,
|
||||
0xAD,0xFB,0xFF,0xAE,0xFB,0xFF,0xFA,0x02,0xFB,0xFE,0xFA,0x01,
|
||||
0xFB,0xFB,0xFA,0xFC,0xFF,0x4D,0x1A,0xF9,0x01,0xF8,0x01,0x00,
|
||||
0x9B,0xFB,0x02,0xA5,0xFB,0xFE,0xFF,0xD2,0x19,0xF9,0x01,0xF8,
|
||||
0xFF,0x00,0x9B,0x9C,0x9D,0x9E,0xFF,0x2E,0x1A,0xF9,0x01,0xF8,
|
||||
0x01,0x00,0xFB,0x02,0xA3,0xFB,0x04,0xA4,0xA5,0xFF,0xD2,0x19,
|
||||
0xF9,0x01,0xF8,0x01,0x00,0xFB,0x06,0xA4,0xA5,0xFF,0xD2,0x19,
|
||||
0xF9,0x01,0xF8,0xFF,0x00,0xFB,0xFD,0xA0,0xFB,0xFE,0x9D,0xFF,
|
||||
0xD2,0x19,0xF9,0x01,0xF8,0xFF,0x00,0xA8,0xF9,0x01,0x97,0xF9,
|
||||
0x01,0x98,0x99,0x9A,0xF9,0x05,0x9B,0xF9,0x01,0x9C,0x9D,0xFF,
|
||||
0xD2,0x19,0xF9,0x01,0xA7,0xFF,0x4A,0x1A,0xA2,0xFF,0x45,0x1A,
|
||||
0xA9,0x96,0xFF,0xD2,0x19,0x9F,0xA0,0xFF,0x5F,0x1A,0xF9,0x01,
|
||||
0xF4,0xFF,0xD2,0x19,0xF9,0x05,0xFB,0xF8,0xFF,0xD2,0x19,0xF9,
|
||||
0x05,0xA0,0x9D,0xFF,0xD2,0x19,0xF9,0x07,0xFB,0xF8,0xFF,0x39,
|
||||
0x1B,0xF9,0x05,0xFE,0xFB,0x05,0xFF,0x2E,0x1A,0xF9,0x05,0xFE,
|
||||
0xFB,0x12,0xFF,0xA6,0x19,0xF9,0x01,0x10,0x11,0xFB,0x02,0x12,
|
||||
0xFB,0x02,0x13,0xFB,0x02,0x14,0xFB,0x02,0x15,0xFB,0x02,0x16,
|
||||
0xFB,0x07,0x17,0xFB,0x09,0x18,0xFB,0x05,0xFA,0xFA,0x19,0xFB,
|
||||
0x01,0xFA,0x06,0x1A,0xFB,0x04,0xF4,0xF2,0x01,0x1B,0xFB,0xFD,
|
||||
0x1C,0xFB,0x05,0x1D,0xF2,0x01,0x1E,0x1F,0x20,0x21,0xFB,0x01,
|
||||
0xFF,0xA0,0x19,0xF9,0x01,0xF2,0x01,0x22,0xFB,0x05,0x23,0xFB,
|
||||
0x06,0x24,0xFB,0x03,0x25,0xFB,0x05,0xF2,0x01,0x26,0xFB,0x07,
|
||||
0x27,0xFB,0x0C,0xFA,0xFD,0x28,0xFB,0x08,0xFA,0xF7,0x29,0xFB,
|
||||
0x08,0xFA,0xFE,0x2A,0xFB,0x04,0xFA,0x0B,0x2B,0xFB,0x04,0xFA,
|
||||
0x03,0x2C,0xFB,0x05,0xF4,0xF2,0x01,0xFF,0x81,0x19,0xF9,0x01,
|
||||
0xFB,0x01,0x6B,0xFB,0x02,0xFB,0x02,0x6C,0xFB,0x02,0x6D,0xFB,
|
||||
0x02,0x6D,0xFB,0x02,0x6D,0xFF,0x16,0x1B,0x00,0xF9,0x01,0xFB,
|
||||
0x01,0x6E,0x6F,0xFB,0x02,0x70,0xFB,0x02,0x6C,0xFB,0x02,0x6D,
|
||||
0xFF,0x29,0x1B,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
|
||||
0xFF,0xC4,0x19,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E,
|
||||
0xFB,0x02,0x2F,0xFB,0xFF,0x30,0xFB,0x01,0x31,0xFB,0xFE,0x32,
|
||||
0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,0xFF,0xFF,0x75,0x19,
|
||||
0xF9,0x01,0xFB,0x01,0x35,0xFB,0x01,0xF2,0x01,0x36,0xFB,0x08,
|
||||
0x37,0xF2,0x01,0x38,0xFB,0x07,0x39,0xFB,0x03,0x3A,0xFB,0x01,
|
||||
0x3B,0x3C,0xFB,0x02,0x3D,0xFB,0xFF,0x3E,0x3F,0x40,0xFB,0xFF,
|
||||
0x41,0xFB,0xF2,0xFE,0xFF,0x95,0x19,0xF9,0x03,0xFA,0xFF,0x66,
|
||||
0xFB,0xFE,0xFA,0x06,0x67,0xFB,0xFE,0xFA,0x09,0x68,0xFB,0xFF,
|
||||
0xFA,0x0C,0x69,0xFB,0xFD,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0xF9,
|
||||
0x03,0xFA,0xFF,0xFB,0xFE,0x66,0xFB,0xFD,0xFA,0x06,0x67,0xFB,
|
||||
0xFD,0xFA,0x09,0x68,0xFB,0xFE,0xFA,0x0C,0x69,0xFB,0xFD,0xF8,
|
||||
0x00,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0xFF,0x66,
|
||||
0xFB,0x02,0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,
|
||||
0x69,0xFB,0xFE,0xF8,0x01,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,
|
||||
0x01,0xFA,0x03,0xF7,0x06,0x1C,0x66,0xFB,0x02,0xFA,0x06,0x67,
|
||||
0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,0xFE,0xF8,0x01,
|
||||
0x0F,0xFF,0x49,0x1D,0xFB,0xFF,0xFA,0xFD,0xFF,0xE4,0x1B,0xFB,
|
||||
0x01,0xFF,0xDB,0x1B,0x66,0xFB,0x02,0xFA,0x03,0x67,0xFB,0xFF,
|
||||
0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,0x01,0x06,0xFF,
|
||||
0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,0x66,0xFB,0x02,0xFA,
|
||||
0x06,0x67,0xFB,0x01,0xFA,0x09,0x68,0xFB,0x02,0xFA,0x0C,0x69,
|
||||
0xF8,0x02,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,
|
||||
0x66,0xFB,0x03,0xFA,0x06,0x67,0xFB,0x02,0xFA,0x09,0x68,0xFB,
|
||||
0x03,0xFA,0x0C,0x69,0xF8,0x03,0x0F,0xFF,0x49,0x1D,0xF9,0x01,
|
||||
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,
|
||||
0x02,0x4E,0x4F,0x50,0xFF,0xA1,0x1C,0xF9,0x01,0x43,0x44,0x45,
|
||||
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,0x02,0xFB,0x01,
|
||||
0x4E,0xFB,0x02,0x4F,0xFB,0x01,0x50,0xFF,0xA1,0x1C,0xF9,0x01,
|
||||
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFB,0xFF,
|
||||
0x4D,0xF9,0x02,0xFB,0xFE,0x4E,0xFB,0xFF,0x4F,0xFB,0xFF,0x50,
|
||||
0xFF,0xA1,0x1C,0xF9,0x02,0x5B,0x5A,0x59,0x58,0x57,0x57,0x57,
|
||||
0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63,
|
||||
0x61,0x60,0x5F,0x5E,0x5D,0x5C,0x5B,0x5A,0x59,0x58,0x57,0x58,
|
||||
0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x5F,0x5E,0x5D,0x5C,
|
||||
0xFF,0x25,0x1D,0xF9,0x06,0x5C,0x5D,0x5D,0x5C,0x5C,0x5B,0xFF,
|
||||
0xD8,0x1C,0x87,0x88,0x89,0x89,0x8A,0x8A,0x8A,0x8A,0x89,0x88,
|
||||
0x87,0xFB,0xF9,0xFF,0x25,0x1D,0xF9,0x01,0x94,0x91,0x90,0x8F,
|
||||
0x8E,0x8D,0xFB,0xFB,0xFA,0x3F,0xFC,0xF9,0x03,0x8C,0x8A,0x88,
|
||||
0x5B,0xF9,0x02,0xFF,0xA4,0x1C,0xF9,0x01,0x87,0x88,0x89,0x8A,
|
||||
0x8B,0x8C,0xFB,0x05,0xFA,0xC1,0xFD,0x8D,0x8E,0x8F,0x90,0x91,
|
||||
0x92,0x93,0x94,0xF9,0x05,0x95,0xF9,0x01,0x76,0x77,0xFB,0x01,
|
||||
0xFF,0xA0,0x19,0x51,0x52,0xF9,0x05,0x53,0xF9,0x01,0xF4,0xF2,
|
||||
0x00,0x54,0x55,0xFB,0x03,0xFF,0xA0,0x19,0xF9,0x03,0x51,0xFA,
|
||||
0x06,0x51,0xFA,0x09,0x51,0xFA,0x0C,0xFB,0x02,0xF8,0x00,0x0C,
|
||||
0xFF,0x49,0x1D,0xF9,0x04,0x6A,0xFF,0x4B,0x1D,0xF9,0x01,0x35,
|
||||
0xFB,0x02,0xF2,0x01,0x36,0xFB,0x07,0x37,0xF2,0x01,0x38,0xFB,
|
||||
0x02,0x31,0xFB,0xFE,0x32,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,
|
||||
0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,
|
||||
0xF9,0x00,0xF5,0x4F,0xFF,0x25,0x1D,0xF9,0x01,0x43,0x44,0x45,
|
||||
0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0xFA,0xFC,
|
||||
0x4F,0xFA,0xFE,0x4F,0x4F,0xFA,0x02,0x4F,0xFA,0x04,0xFF,0x25,
|
||||
0x1D,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFA,
|
||||
0xFF,0x4D,0xFA,0xFD,0x4E,0xFA,0xFC,0x4F,0xFA,0xF6,0x4F,0xFA,
|
||||
0xF7,0x4F,0xFA,0xF8,0x4F,0xFA,0xF9,0x4F,0xFA,0xFA,0x4F,0xFA,
|
||||
0xFB,0x4F,0xFA,0xFC,0x4F,0xFA,0xFD,0x4F,0xFA,0xFE,0x4F,0xFA,
|
||||
0xFE,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0x4F,
|
||||
0x4F,0x4F,0xFA,0x01,0x4F,0xFA,0x01,0x4F,0xFA,0x02,0x4F,0xFA,
|
||||
0x02,0x4F,0xFA,0x03,0x4F,0xFA,0x04,0x4F,0xFA,0x05,0x4F,0xFA,
|
||||
0x06,0x4F,0xF8,0x00,0x06,0xFF,0x49,0x1D,0xF9,0x03,0x50,0xFF,
|
||||
0xA1,0x1C,0xF9,0x05,0xFB,0xFC,0x32,0x33,0x34,0xFF,0xA0,0x19,
|
||||
0xF9,0x05,0xFB,0x01,0xFA,0x03,0xF7,0x25,0x1E,0x66,0xFB,0x02,
|
||||
0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,
|
||||
0xFE,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0x66,0xFB,0x02,0xFA,0x03,
|
||||
0x67,0xFB,0xFF,0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,
|
||||
0x00,0x06,0xFF,0x49,0x1D,0xF9,0x05,0xFB,0xFF,0xFA,0xFC,0x66,
|
||||
0xFB,0xFF,0xFA,0x03,0xFB,0xFD,0xFA,0x01,0xF4,0xFB,0x01,0xF2,
|
||||
0x01,0x6B,0xFB,0x02,0x6C,0xF2,0x01,0x6D,0xFF,0xB3,0x1F,0x79,
|
||||
0xFB,0x01,0x7A,0x7B,0xFB,0x02,0x7C,0xFB,0x04,0x7D,0xFB,0x03,
|
||||
0x7E,0xFB,0xFC,0x56,0xF2,0x01,0xF4,0xFB,0xFC,0x74,0xFB,0xFE,
|
||||
0x75,0x76,0x77,0xFF,0xA0,0x19,0xFB,0xFB,0xFF,0xA0,0x19,0xF9,
|
||||
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
|
||||
0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x03,0x7F,0x80,0x81,
|
||||
0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,
|
||||
0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,
|
||||
0xFB,0xFF,0xFB,0x02,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
|
||||
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
|
||||
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x01,0x7F,
|
||||
0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
|
||||
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,
|
||||
0x03,0x7E,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
|
||||
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
|
||||
0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFE,0x80,0x81,0x82,
|
||||
0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFF,0xFC,0x1E,0xF9,
|
||||
0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,
|
||||
0x04,0x7D,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,
|
||||
0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,
|
||||
0x7C,0xFB,0x02,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,
|
||||
0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x02,
|
||||
0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,
|
||||
0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x01,0x81,0x82,0x83,
|
||||
0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,
|
||||
0x7B,0xFB,0x02,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,
|
||||
0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x01,0x83,0x84,0xFF,0xA0,0x19,
|
||||
0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x84,0xFF,0xA0,0x19,
|
||||
0xF9,0x01,0x79,0xFB,0x01,0x84,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
|
||||
0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFF,0xAF,0x1F,0xF9,0x05,0xFB,
|
||||
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0x72,0x73,0x74,
|
||||
0xFB,0xFC,0x75,0x76,0x77,0xFF,0xA0,0x19,0xF9,0x01,0xF3,0x01,
|
||||
0xE5,0xE5,0xE5,0xE5,0xE5,0xE5,0xE6,0xE7,0xE8,0xFF,0xDA,0x1F,
|
||||
0xF9,0x01,0xFB,0xFB,0xE9,0xEA,0xEB,0xEC,0xED,0xEE,0xEF,0xF0,
|
||||
0x85,0x85,0x86,0x86,0x86,0x30,0xFB,0x01,0x31,0xFB,0xFE,0xF9,
|
||||
0x05,0x32,0xF9,0x01,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,
|
||||
0xFB,0xEA,0xEC,0xEE,0xF0,0x86,0xFB,0xFF,0xFF,0xA0,0x19,0xF9,
|
||||
0x01,0xFB,0x04,0xBF,0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xF2,0x03,
|
||||
0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCD,0xCD,0xF3,0x01,
|
||||
0xCD,0xCD,0xC9,0xC6,0xFB,0xFC,0xFF,0xA0,0x19,0xF9,0x05,0xF4,
|
||||
0xFB,0x01,0x6B,0xFB,0x02,0x6C,0xF9,0x01,0x6D,0xFF,0x36,0x20,
|
||||
0xF9,0x01,0xFA,0xFE,0xFB,0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFB,
|
||||
0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,0x01,0x72,
|
||||
0xFA,0x01,0x73,0xFB,0xFE,0xFF,0x78,0x19,0xF9,0x05,0xF4,0xFA,
|
||||
0xFE,0xFB,0x01,0xFB,0x02,0x6C,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
|
||||
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,
|
||||
0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0xFB,
|
||||
0x01,0x6E,0x6E,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,
|
||||
0x01,0x72,0xFA,0x01,0x73,0x74,0xFB,0xFC,0x75,0x76,0x77,0xFF,
|
||||
0xA0,0x19,0xF9,0x05,0xF4,0xFA,0xFE,0xFB,0x03,0xB9,0xF6,0xB9,
|
||||
0xFF,0xA5,0x20,0xF9,0x05,0xFF,0xAE,0x20,0xF9,0x01,0xF6,0xB3,
|
||||
0xB4,0xB5,0xB6,0xFB,0x01,0xB7,0xFB,0xFC,0xB9,0xFF,0xBA,0x20,
|
||||
0xF9,0x01,0xF4,0xFB,0x04,0xB1,0xF6,0xB1,0xFF,0xC5,0x20,0xF9,
|
||||
0x01,0xB2,0xF6,0xB2,0xFF,0xCD,0x20,0xFF,0x5A,0x20,0xF8,0x00,
|
||||
0x00,0xF9,0x04,0xB9,0xFF,0xD9,0x20,0xF9,0x05,0xFB,0xFB,0xFA,
|
||||
0xFF,0xF2,0x01,0xD9,0xDA,0xDB,0xFB,0x01,0xDC,0xFB,0xFC,0xFA,
|
||||
0xFD,0xF2,0x01,0xDD,0xFB,0xFC,0xFA,0xFE,0xDE,0xFB,0xFE,0xFA,
|
||||
0xFD,0xDF,0xFB,0xFD,0xFA,0xF8,0xF2,0x04,0xF2,0x01,0xE0,0xFB,
|
||||
0xFF,0xFA,0xFF,0xE1,0xFB,0xFD,0xFA,0xFC,0xE2,0xFB,0xFF,0xFA,
|
||||
0xFB,0xF2,0x01,0xE3,0xFB,0xFE,0xFA,0xFF,0xE4,0x00,0xF2,0x01,
|
||||
0x00,0x00,0x00,0xF2,0x01,0x00,0x00,0x00,0xF2,0x01,0x00,0x00,
|
||||
0x00,0xF2,0x01,0xF1,0x00,0xFF,0x2E,0x21,0x36,0xFF,0x32,0x21,
|
||||
0x55,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x44,
|
||||
0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x53,0x54,0x4C,0xFF,0x4B,
|
||||
0x21,0xFB,0x01,0x30,0xFB,0x02,0x31,0xFB,0x06,0x32,0xFB,0x01,
|
||||
0x33,0xFB,0xFF,0x34,0xFB,0x01,0x35,0xFB,0x01,0xFF,0x51,0x21,
|
||||
0xFB,0x01,0x37,0x38,0xFF,0x32,0x21,0x4D,0x4E,0x4F,0x50,0x51,
|
||||
0x52,0xFB,0x01,0x36,0x36,0x36,0x36,0x36,0x36,0x39,0x3A,0x3B,
|
||||
0x3C,0x3D,0xFB,0x02,0x3E,0xFB,0xFF,0x3F,0xFB,0xFD,0x40,0x41,
|
||||
0xFB,0xFF,0x42,0xFE,0xFB,0x10,0xFB,0x03,0xFF,0x54,0x21,0x0B,
|
||||
0xFF,0x95,0x21,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFE,
|
||||
0xFB,0x08,0x0B,0xFF,0x95,0x21,0xFE,0xFB,0x0B,0x0C,0xFB,0x01,
|
||||
0x0D,0xFB,0x01,0x0E,0xFB,0x03,0x0F,0xFB,0x01,0x10,0x11,0xFF,
|
||||
0xB8,0x21,0x13,0xFF,0xBC,0x21,0x14,0xFF,0xC0,0x21,0x15,0xFB,
|
||||
0x01,0x16,0x17,0x18,0xFB,0x01,0x19,0xFB,0xFD,0x1A,0xFB,0xFE,
|
||||
0x1B,0xFB,0xFC,0x1C,0xFB,0xFD,0x1D,0xFB,0xFE,0x1E,0xFB,0xFD,
|
||||
0x1F,0xFB,0xFF,0x20,0x21,0xFF,0xE2,0x21,0x25,0xFF,0xE6,0x21,
|
||||
0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0xFE,
|
||||
0xFB,0x0C,0x0B,0xFF,0xF8,0x21,0x0B,0x0B,0xFE,0xFB,0x0D,0x2F,
|
||||
0x2E,0x2D,0x2C,0x2B,0x2A,0x29,0x28,0x27,0x26,0x25,0x24,0x24,
|
||||
0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,
|
||||
0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,
|
||||
0x22,0x22,0x23,0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,
|
||||
0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,0x23,0x23,0x24,0xFF,
|
||||
0x3C,0x22,0x01,0x12,0xFF,0x41,0x22,0xF9,0x01,0xBA,0xFB,0x05,
|
||||
0xBA,0xFB,0x03,0xBB,0xFB,0x04,0xFF,0x47,0x22,0xBA,0xFF,0x53,
|
||||
0x22,0xBC,0xFF,0x57,0x22,0xF9,0x00,0xBA,0xBA,0xBA,0xBC,0xBC,
|
||||
0xBC,0xBC,0xBC,0xBC,0xBC,0xBC,0xFE,0xFB,0x08,0xFF,0x47,0x22,
|
||||
0xBA,0xBA,0xBC,0xFF,0x70,0x22,
|
||||
};
|
||||
|
||||
/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */
|
||||
#define KID_BIN_FRAMES_OFF 0
|
||||
#define KID_BIN_SEQTBL_OFF 1205
|
||||
/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс
|
||||
* frame + add_frame - 149 (add_frame=70 для кадров 102..106). */
|
||||
#define KID_NGFRAMES 41
|
||||
#define KID_BIN_GFRAMES_OFF 3515
|
||||
|
||||
#define KID_NSEQ 115
|
||||
static const uint16_t kid_seq_off[KID_NSEQ] = {
|
||||
@@ -471,4 +42,31 @@ static const uint16_t kid_seq_off[KID_NSEQ] = {
|
||||
0x21FC,0x2240,0x2257,
|
||||
};
|
||||
|
||||
/* 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
|
||||
|
||||
@@ -13,13 +13,38 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
|
||||
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
|
||||
* POP_YOFF = (256-200)/2 = 28px (чёрные борта сверху/снизу). Внутри 200px
|
||||
* верхние POP_PLAYFIELD_H = 192 — поле тайлов/спрайтов (комната = 3 ряда),
|
||||
* нижние 8 — статус-полоса (жизни/сообщения): тайлы/спрайты туда НЕ лезут.
|
||||
* Сдвиг применяется на границе примитивов (blit/heal) — логика рисования
|
||||
* остаётся в «room-space» y=0..191. */
|
||||
#define POP_YOFF 28
|
||||
#define POP_PLAYFIELD_H 192
|
||||
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
|
||||
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
|
||||
|
||||
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
|
||||
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
|
||||
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
|
||||
* того, вылезает край или нет.
|
||||
* static inline, а не чистый inline: SDCC оставляет мёртвое тело в каждом TU
|
||||
* (memory sdcc_inline_codegen_findings), но здесь это ~30 байт на два TU —
|
||||
* дешевле риска неразрешённой ссылки. */
|
||||
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);
|
||||
}
|
||||
|
||||
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
|
||||
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
|
||||
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
|
||||
int pop_bg_load(void);
|
||||
int pop_bg_load(void) __banked;
|
||||
|
||||
/* Освободить EMM-страницы атласов. */
|
||||
void pop_bg_free(void);
|
||||
void pop_bg_free(void) __banked;
|
||||
|
||||
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
|
||||
* (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца
|
||||
@@ -27,13 +52,114 @@ void pop_bg_free(void);
|
||||
* тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */
|
||||
void pop_room_draw(uint8_t room_num,
|
||||
const uint8_t *fg, const uint8_t *bg,
|
||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg);
|
||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked;
|
||||
|
||||
/* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright,
|
||||
* когда тайл нижнего ряда стал empty (после падения loose): «стена вниз»
|
||||
* рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */
|
||||
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
|
||||
|
||||
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
||||
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
|
||||
* комнаты сверху (кромка уровня → пол). */
|
||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
|
||||
|
||||
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
|
||||
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
|
||||
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
|
||||
* берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */
|
||||
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir, uint8_t frame, uint8_t action);
|
||||
int8_t dir, uint8_t frame, uint8_t action) __banked;
|
||||
|
||||
/* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с
|
||||
* POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит
|
||||
* тот, кто нарисовал спрайт (kid_draw / pop_guard_draw) — он один знает
|
||||
* итоговый габарит (у Kid — с учётом клинка и обрезки clip_char). Вне
|
||||
* fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и
|
||||
* замер — в pop_bg.c у fclip_on. */
|
||||
void pop_fore_set_clip(int x, int y, int w, int h) __banked;
|
||||
|
||||
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
|
||||
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
|
||||
* (до ×8/7), как у kid_fp_obj_x(). */
|
||||
/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому
|
||||
* вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в
|
||||
* кадр, других вызывающих нет — цена одного трамплина здесь не видна. */
|
||||
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir) __banked;
|
||||
|
||||
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
|
||||
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
|
||||
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
|
||||
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
|
||||
void pop_fore_heal(void) __banked;
|
||||
|
||||
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
|
||||
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
|
||||
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
|
||||
* после падения (NORMAL, в ОЗУ-копию). */
|
||||
void pop_loose_shake_draw(int row, int col) __banked;
|
||||
void pop_loose_bake_empty(int row, int col) __banked;
|
||||
|
||||
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
|
||||
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
|
||||
* Состояние — pop_ceil_modif[10] в pop_map. */
|
||||
void pop_ceil_shake_draw(int col) __banked;
|
||||
void pop_ceil_bake_empty(int col) __banked;
|
||||
|
||||
/* Кусок приземлился (loose_land) — переехал в pop_state.h: переменную
|
||||
* читают по обе стороны банковой границы. */
|
||||
#include "pop_state.h"
|
||||
|
||||
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
|
||||
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
|
||||
void pop_spike_redraw(int row, int col) __banked;
|
||||
|
||||
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
|
||||
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
|
||||
* (пламя рисуется в ячейке правого соседа). */
|
||||
void pop_torch_draw(int row, int col, uint8_t modif) __banked;
|
||||
void pop_potion_draw(int row, int col, uint8_t modif) __banked;
|
||||
|
||||
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
|
||||
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
|
||||
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
|
||||
void pop_room_redraw_seam_left(void) __banked;
|
||||
|
||||
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
|
||||
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
|
||||
* кнопка нажата/отжата, floor → debris от упавшей плиты. */
|
||||
void pop_floor_bake(int row, int col) __banked;
|
||||
void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кнопка) */
|
||||
|
||||
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
|
||||
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
|
||||
void pop_leveldoor_redraw(int row, int col) __banked;
|
||||
|
||||
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
|
||||
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
|
||||
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
|
||||
void pop_loose_mob_spawn(int row, int col) __banked;
|
||||
void pop_loose_mob_tick(void) __banked;
|
||||
void pop_loose_mob_reset(void) __banked; /* остановить кусок (смена комнаты) */
|
||||
|
||||
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
|
||||
* seg008:1561). Звать после kid_draw, перед fore-слоем. */
|
||||
void pop_loose_mob_draw_over(void) __banked;
|
||||
|
||||
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
|
||||
uint8_t pop_loose_mob_pos(int *col, int *y) __banked;
|
||||
|
||||
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
|
||||
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
|
||||
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
|
||||
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
|
||||
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked;
|
||||
|
||||
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
|
||||
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
|
||||
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
|
||||
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
|
||||
void pop_room_clip_borders(void) __banked;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* pop_char.h — состояние ПЕРСОНАЖА, общее для Кида, стража и активной
|
||||
* копии Char. Порт char_type (types.h:302).
|
||||
*
|
||||
* Зачем отдельным заголовком: интерпретатор последовательностей в
|
||||
* оригинале один на всех (работает с Char, а вокруг стоят
|
||||
* loadkid/savekid и loadshad/saveshad, seg006:809). Пока у Кида был свой
|
||||
* усечённый тип, а у стража свой, общий интерпретатор был невозможен.
|
||||
* Поля и их порядок — как в char_type, чтобы порт seg005/seg006 ложился
|
||||
* один в один.
|
||||
*/
|
||||
#ifndef POP_CHAR_H
|
||||
#define POP_CHAR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t frame;
|
||||
uint8_t x, y;
|
||||
int8_t direction; /* -1 влево, 0 вправо */
|
||||
int8_t curr_col, curr_row;
|
||||
uint8_t action;
|
||||
int8_t fall_x, fall_y;
|
||||
uint8_t room;
|
||||
uint8_t repeat;
|
||||
uint8_t charid; /* 0 = Kid, 2 = страж (см. CHARID_* в pop_guard.h) */
|
||||
uint8_t sword;
|
||||
int8_t alive; /* -1 = жив (как в оригинале), 0 = убит */
|
||||
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
|
||||
} pop_char_t;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* pop_cheat.h — режим читов (порт cheats_enabled, seg000:111).
|
||||
*
|
||||
* В оригинале включается ключом командной строки megahit/debug и меняет
|
||||
* набор доступных клавиш. У нас конфигурации/UI пока нет, поэтому это
|
||||
* просто глобальный флаг: main ставит его при старте (на время разработки —
|
||||
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
|
||||
*
|
||||
* Реализовано: K — убить стража (seg000:786). Остальные читы оригинала
|
||||
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
|
||||
*/
|
||||
#ifndef POP_CHEAT_H
|
||||
#define POP_CHEAT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
|
||||
|
||||
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
|
||||
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
|
||||
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
|
||||
|
||||
#endif
|
||||
@@ -19,6 +19,13 @@
|
||||
#include <kbd_raw.h>
|
||||
#include "pop_kid.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_guard.h" /* charid, состояние меча, seq стража */
|
||||
|
||||
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
|
||||
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
|
||||
* scancodes. TODO: убрать после отладки. */
|
||||
#define KBD_DBG_STEPL 0x3B /* J */
|
||||
#define KBD_DBG_STEPR 0x4B /* L */
|
||||
|
||||
/* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */
|
||||
#define CONTROL_RELEASED 0
|
||||
@@ -45,6 +52,8 @@
|
||||
#define SEQ_49_STAND_UP_FROM_CROUCH 49
|
||||
#define SEQ_50_CROUCH 50
|
||||
#define SEQ_79_CROUCH_HOP 79
|
||||
#define SEQ_78_DRINK 78 /* выпить зелье */
|
||||
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
|
||||
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
|
||||
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
|
||||
#define SEQ_44_STEP_ON_EDGE 44
|
||||
@@ -67,14 +76,21 @@
|
||||
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
|
||||
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между
|
||||
* кадрами (rest/save). */
|
||||
static int8_t control_x, control_y, control_shift;
|
||||
static int8_t control_forward, control_backward, control_up, control_down, control_shift2;
|
||||
int8_t control_x, control_y, control_shift;
|
||||
/* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт
|
||||
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же
|
||||
* control(), что и у игрока. Поэтому шина общая. */
|
||||
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
|
||||
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
|
||||
|
||||
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
|
||||
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
|
||||
* не дублируются в этот TU). */
|
||||
#define seqtbl_offset_char(seq) kid_set_seq(seq)
|
||||
/* В оригинале seqtbl_offset_char работает с АКТИВНЫМ персонажем, и
|
||||
* control() вызывается внутри окна Char. Раньше макрос вёл на
|
||||
* kid_set_seq (пишет прямо в Kid) — после введения окна это ломало ввод:
|
||||
* savekid затирал Kid копией Char со старой curr_seq. */
|
||||
#define seqtbl_offset_char(seq) pop_char_set_seq(seq)
|
||||
|
||||
static int8_t release_arrows(void)
|
||||
{
|
||||
@@ -98,17 +114,39 @@ static void standing_jump(void)
|
||||
seqtbl_offset_char(SEQ_3_STANDING_JUMP);
|
||||
}
|
||||
|
||||
/* check_get_item (seg005:061F): решение по тайлам принимает pop_map, здесь
|
||||
* только исполнение. Триггер в оригинале — Shift (control_shift2) в стойке
|
||||
* или в приседе: первый раз Kid приседает к предмету, второй — поднимает.
|
||||
* Возврат 1 = управление обработано (caller делает return). */
|
||||
static uint8_t get_item_action(void)
|
||||
{
|
||||
uint8_t a = pop_get_item_action();
|
||||
if (a == 1) { crouch(); return 1; }
|
||||
if (a == 2) {
|
||||
control_shift2 = CONTROL_IGNORE; /* do_pickup: без авто-повтора */
|
||||
seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
|
||||
return 1;
|
||||
}
|
||||
if (a == 3) { /* зелье — выпить (get_item) */
|
||||
control_shift2 = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(SEQ_78_DRINK);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void run_jump(void)
|
||||
{
|
||||
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
|
||||
* полировка K3; K2b просто запускает run-jump. */
|
||||
if (Kid.frame >= FRAME_7_RUN) {
|
||||
if (Char.frame >= FRAME_7_RUN) {
|
||||
control_up = release_arrows();
|
||||
seqtbl_offset_char(SEQ_4_RUN_JUMP);
|
||||
}
|
||||
}
|
||||
|
||||
static void safe_step(void); /* fwd */
|
||||
static void draw_sword(void); /* fwd: нужен в control_standing */
|
||||
|
||||
static void forward_pressed(void)
|
||||
{
|
||||
@@ -167,15 +205,15 @@ static void safe_step(void)
|
||||
control_forward = CONTROL_IGNORE;
|
||||
distance = pop_edge_distance();
|
||||
if (distance) {
|
||||
Kid.repeat = 1;
|
||||
Char.repeat = 1;
|
||||
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
|
||||
} else if (pop_edge_type() == EDGE_WALL) {
|
||||
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
|
||||
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
|
||||
Kid.repeat = 1;
|
||||
Char.repeat = 1;
|
||||
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
|
||||
} else if (Kid.repeat != 0) {
|
||||
Kid.repeat = 0;
|
||||
} else if (Char.repeat != 0) {
|
||||
Char.repeat = 0;
|
||||
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
|
||||
} else {
|
||||
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
|
||||
@@ -185,6 +223,8 @@ static void safe_step(void)
|
||||
/* ---- control_* обработчики состояний ------------------------------ */
|
||||
static void control_crouched(void)
|
||||
{
|
||||
/* Shift в приседе — поднять предмет (seg005 control_crouched). */
|
||||
if (control_shift2 == CONTROL_HELD && get_item_action()) return;
|
||||
if (control_y != CONTROL_HELD_DOWN) {
|
||||
seqtbl_offset_char(SEQ_49_STAND_UP_FROM_CROUCH);
|
||||
} else if (control_forward == CONTROL_HELD) {
|
||||
@@ -195,7 +235,37 @@ static void control_crouched(void)
|
||||
|
||||
static void control_standing(void)
|
||||
{
|
||||
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */
|
||||
/* Shift в стойке над предметом — присесть к нему (seg005:0358, первая
|
||||
* строка control_standing). Идёт ДО остальных shift-веток. */
|
||||
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
|
||||
get_item_action())
|
||||
return;
|
||||
/* Ветку «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_backward == CONTROL_HELD) back_pressed();
|
||||
else if (control_up == CONTROL_HELD) up_pressed();
|
||||
@@ -236,7 +306,7 @@ static void control_running(void)
|
||||
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
|
||||
* seq_47 при беге в стену (как оригинал), с корректной позицией. */
|
||||
if (control_x == CONTROL_RELEASED &&
|
||||
(Kid.frame == FRAME_7_RUN || Kid.frame == FRAME_11_RUN)) {
|
||||
(Char.frame == FRAME_7_RUN || Char.frame == FRAME_11_RUN)) {
|
||||
control_forward = release_arrows();
|
||||
seqtbl_offset_char(SEQ_13_STOP_RUN);
|
||||
} else if (control_x == CONTROL_HELD_BACKWARD) {
|
||||
@@ -287,11 +357,216 @@ static void control_jumpup(void)
|
||||
standing_jump();
|
||||
}
|
||||
|
||||
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */
|
||||
static void control(void)
|
||||
/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
|
||||
* клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
|
||||
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
|
||||
* уже как выпад. */
|
||||
static void draw_sword(void)
|
||||
{
|
||||
uint8_t f = Kid.frame;
|
||||
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
|
||||
uint8_t seq_id = SEQ_55_DRAW_SWORD;
|
||||
control_forward = control_shift2 = release_arrows();
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
|
||||
} else if (Char.charid != CHARID_1_SHADOW) {
|
||||
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
|
||||
}
|
||||
Char.sword = SWORD_2_DRAWN;
|
||||
seqtbl_offset_char(seq_id);
|
||||
}
|
||||
|
||||
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
|
||||
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч,
|
||||
* вниз без «вперёд» — развернуться (stand flipped). */
|
||||
static void control_guard_inactive(void)
|
||||
{
|
||||
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
|
||||
if (control_forward == CONTROL_HELD) {
|
||||
draw_sword();
|
||||
} else {
|
||||
control_down = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Боёвка (seg005:964..) ---------------------------------------- *
|
||||
* Всё это ветки ОБЩЕГО диспетчера: у стража тот же control(), просто
|
||||
* control_* ему выставляет ИИ (autocontrol_*, guards.c), а не клавиатура. */
|
||||
|
||||
/* back_with_sword (seg005): шаг назад — только из стоек. */
|
||||
static void back_with_sword(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||
f == FRAME_171_STAND_WITH_SWORD) {
|
||||
control_backward = CONTROL_IGNORE; /* без авто-повтора */
|
||||
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD);
|
||||
}
|
||||
}
|
||||
|
||||
/* forward_with_sword (seg005): шаг вперёд; у стража своя последовательность. */
|
||||
static void forward_with_sword(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||
f == FRAME_171_STAND_WITH_SWORD) {
|
||||
control_forward = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(Char.charid != CHARID_0_KID ? SEQ_56_GUARD_FORWARD
|
||||
: SEQ_86_FORWARD_WITH_SWORD);
|
||||
}
|
||||
}
|
||||
|
||||
/* sword_strike (seg005): выпад из стойки/шага либо добивание после парирования. */
|
||||
static void sword_strike(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
uint8_t seq_id;
|
||||
if (f == FRAME_157_WALK_WITH_SWORD || f == FRAME_158_STAND_WITH_SWORD ||
|
||||
f == FRAME_170_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD ||
|
||||
f == FRAME_165_WALK_WITH_SWORD) {
|
||||
seq_id = (Char.charid == CHARID_0_KID) ? SEQ_75_STRIKE : SEQ_58_GUARD_STRIKE;
|
||||
} else if (f == FRAME_150_PARRY || f == FRAME_161_PARRY) {
|
||||
seq_id = SEQ_66_STRIKE_AFTER_PARRY;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
control_shift2 = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(seq_id);
|
||||
}
|
||||
|
||||
/* parry (seg005): блок. Условие сильно разное у Кида и у соперника —
|
||||
* соперник блокирует ТОЛЬКО на кадре 152 (второй кадр удара), а Кид ещё и
|
||||
* по 153 (и тогда последовательность прокручивается сразу). */
|
||||
static void parry(void)
|
||||
{
|
||||
uint8_t cf = Char.frame, of = Opp.frame;
|
||||
uint8_t seq_id = SEQ_62_PARRY;
|
||||
uint8_t do_play_seq = 0;
|
||||
|
||||
if (cf == FRAME_158_STAND_WITH_SWORD || cf == FRAME_170_STAND_WITH_SWORD ||
|
||||
cf == FRAME_171_STAND_WITH_SWORD || cf == FRAME_168_BACK ||
|
||||
cf == FRAME_165_WALK_WITH_SWORD) {
|
||||
if (pop_char_opp_dist() >= 32 && Char.charid != CHARID_0_KID) {
|
||||
back_with_sword();
|
||||
return;
|
||||
} else if (Char.charid == CHARID_0_KID) {
|
||||
if (of == FRAME_168_BACK) return;
|
||||
if (of != FRAME_151_STRIKE_1 && of != FRAME_152_STRIKE_2 &&
|
||||
of != FRAME_162_BLOCK_TO_STRIKE) {
|
||||
if (of == FRAME_153_STRIKE_3) do_play_seq = 1;
|
||||
}
|
||||
} else {
|
||||
if (of != FRAME_152_STRIKE_2) return;
|
||||
}
|
||||
} else {
|
||||
if (cf != FRAME_167_BLOCKED) return;
|
||||
seq_id = SEQ_61_PARRY_AFTER_STRIKE;
|
||||
}
|
||||
control_up = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(seq_id);
|
||||
if (do_play_seq) play_seq();
|
||||
}
|
||||
|
||||
/* swordfight (seg005:0CDB) — разбор ввода в бою. Порядок веток важен:
|
||||
* удар старше «убрать меч», тот старше блока, блок старше шагов. */
|
||||
static void swordfight(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
uint8_t seq_id;
|
||||
|
||||
if (f == FRAME_161_PARRY && control_shift2 >= CONTROL_RELEASED) {
|
||||
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD); /* отскок после парирования */
|
||||
return;
|
||||
} else if (control_shift2 == CONTROL_HELD) {
|
||||
if (Char.charid == CHARID_0_KID) kid_sword_strike = 15;
|
||||
sword_strike();
|
||||
if (control_shift2 == CONTROL_IGNORE) return;
|
||||
}
|
||||
|
||||
if (control_down == CONTROL_HELD) {
|
||||
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||
f == FRAME_171_STAND_WITH_SWORD) {
|
||||
control_down = CONTROL_IGNORE;
|
||||
Char.sword = SWORD_0_SHEATHED;
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
offguard = 1; /* убрал намеренно — сразу не доставать */
|
||||
guard_refrac = 9; /* и страж на это время придержан */
|
||||
holding_sword = 0;
|
||||
seq_id = SEQ_93_PUT_SWORD_AWAY_FAST;
|
||||
} else if (Char.charid == CHARID_1_SHADOW) {
|
||||
seq_id = SEQ_92_PUT_SWORD_AWAY;
|
||||
} else {
|
||||
seq_id = SEQ_87_GUARD_BECOME_INACT; /* Кид ушёл из виду */
|
||||
}
|
||||
seqtbl_offset_char(seq_id);
|
||||
}
|
||||
} else if (control_up == CONTROL_HELD) {
|
||||
parry();
|
||||
} else if (control_forward == CONTROL_HELD) {
|
||||
forward_with_sword();
|
||||
} else if (control_backward == CONTROL_HELD) {
|
||||
back_with_sword();
|
||||
}
|
||||
}
|
||||
|
||||
/* control_with_sword (seg005:964): пока соперник виден и близко — бой;
|
||||
* иначе страж убирает меч, а Кид просто стоит. */
|
||||
static void control_with_sword(void)
|
||||
{
|
||||
if (Char.action >= 2) return; /* actions_2_hang_climb и выше */
|
||||
if (pop_tile_at(Char.curr_col, Char.curr_row) == 11 /* loose */ ||
|
||||
can_guard_see_kid >= 2) {
|
||||
int16_t d = pop_char_opp_dist();
|
||||
if (d < 90) {
|
||||
if (d >= 0) { swordfight(); return; }
|
||||
/* Соперник за спиной: далеко — развернуться, вплотную — драться. */
|
||||
if (d < -4) { seqtbl_offset_char(SEQ_60_TURN_WITH_SWORD); return; }
|
||||
swordfight();
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Не видит / далеко. */
|
||||
if (Char.charid < CHARID_2_GUARD) {
|
||||
if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
|
||||
Char.sword = SWORD_0_SHEATHED;
|
||||
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); /* страж убит */
|
||||
}
|
||||
} else {
|
||||
swordfight();
|
||||
}
|
||||
}
|
||||
|
||||
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
|
||||
void pop_control(void)
|
||||
{
|
||||
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) и в свободном падении управление
|
||||
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
|
||||
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
|
||||
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
|
||||
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
|
||||
* пару пикселей меньше, чем должен»). */
|
||||
if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
|
||||
release_arrows();
|
||||
return;
|
||||
}
|
||||
/* Порядок веток — как в 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();
|
||||
else if (f == FRAME_48_TURN)
|
||||
control_turning();
|
||||
@@ -314,9 +589,14 @@ static void read_input(void)
|
||||
uint8_t r = kbd_raw_down(KBD_RIGHT);
|
||||
uint8_t u = kbd_raw_down(KBD_UP);
|
||||
uint8_t d = kbd_raw_down(KBD_DOWN);
|
||||
/* Отладочный осторожный шаг: J/L = Shift+влево/вправо (см. KBD_DBG_STEP*). */
|
||||
uint8_t sl = kbd_raw_down(KBD_DBG_STEPL);
|
||||
uint8_t sr = kbd_raw_down(KBD_DBG_STEPR);
|
||||
if (sl) l = 1;
|
||||
if (sr) r = 1;
|
||||
control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED);
|
||||
control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED);
|
||||
control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
|
||||
control_shift = (sl || sr || kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
|
||||
? CONTROL_HELD : CONTROL_RELEASED;
|
||||
}
|
||||
|
||||
@@ -361,12 +641,12 @@ static void flip_control_x(void)
|
||||
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
|
||||
static void user_control(void)
|
||||
{
|
||||
if (Kid.direction >= DIR_RIGHT) {
|
||||
if (Char.direction >= DIR_RIGHT) {
|
||||
flip_control_x();
|
||||
control();
|
||||
pop_control();
|
||||
flip_control_x();
|
||||
} else {
|
||||
control();
|
||||
pop_control();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +686,12 @@ void pop_ctrl_tick(void)
|
||||
rest_ctrl_1();
|
||||
read_input();
|
||||
read_user_control();
|
||||
user_control(); /* -> control(): может сменить Kid.curr_seq */
|
||||
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
|
||||
* персонажем (как в оригинале), поэтому ввод игрока применяется к
|
||||
* Киду только внутри loadkid/savekid. Кадр не перезагружаем —
|
||||
* play_seq в kid_tick сделает это следующим шагом. */
|
||||
pop_loadkid();
|
||||
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
|
||||
pop_savekid_state();
|
||||
save_ctrl_1();
|
||||
}
|
||||
|
||||
@@ -32,4 +32,27 @@ void pop_ctrl_tick(void);
|
||||
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
|
||||
int8_t pop_ctrl_shift_held(void);
|
||||
|
||||
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
|
||||
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
|
||||
* диспетчера: он ВЫСТАВЛЯЕТ эти переменные, а дальше исполняется тот же
|
||||
* control(), что и для игрока. Значения — CONTROL_RELEASED/IGNORE/HELD. */
|
||||
#define POP_CONTROL_RELEASED 0
|
||||
#define POP_CONTROL_IGNORE 1
|
||||
#define POP_CONTROL_HELD (-1)
|
||||
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
|
||||
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
|
||||
* заполняет как есть, без flip_control_x. */
|
||||
extern int8_t control_x, control_y, control_shift;
|
||||
#define POP_CONTROL_HELD_FORWARD (-1)
|
||||
#define POP_CONTROL_HELD_BACKWARD 1
|
||||
#define POP_CONTROL_HELD_UP (-1)
|
||||
#define POP_CONTROL_HELD_DOWN 1
|
||||
extern int8_t control_forward, control_backward, control_up, control_down,
|
||||
control_shift2;
|
||||
|
||||
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
|
||||
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из
|
||||
* своего тика напрямую. */
|
||||
void pop_control(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* pop_gdraw.c — ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c).
|
||||
*
|
||||
* Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ
|
||||
* целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны
|
||||
* остаться в W1/W2 — из банка (guards.c) резидент W3 недостижим ни прямо,
|
||||
* ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит
|
||||
* ровно между «что рисует» (зовёт только главный цикл → резидент) и «что
|
||||
* знает» (зовёт и банк → W1/W2).
|
||||
*
|
||||
* Писучие статики резидента (--dataseg не трогаем) остаются в обычном
|
||||
* _DATA, то есть в W2 — сюда переезжает КОД, не данные.
|
||||
*/
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_guard.h"
|
||||
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
|
||||
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
|
||||
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
|
||||
#include "kid_data.h"
|
||||
|
||||
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
|
||||
#define GUARD_PAGES 5
|
||||
static atlas_t gp[GUARD_PAGES];
|
||||
static uint8_t g_ok;
|
||||
|
||||
/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid:
|
||||
* heal обязан стирать спрайт именно той страницы, в которую рисуем). */
|
||||
static int gx_l[2], gy_l[2];
|
||||
static uint16_t gw_l[2], gh_l[2];
|
||||
static uint8_t gvalid[2];
|
||||
|
||||
/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение
|
||||
* почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит
|
||||
* ровно по площади. Объединение считаем только для окна fore-клипа. */
|
||||
static int sx_l[2], sy_l[2];
|
||||
static uint16_t sw_l[2], sh_l[2];
|
||||
static uint8_t svalid[2];
|
||||
|
||||
int pop_guard_load(void) __banked
|
||||
{
|
||||
static const char *const names[GUARD_PAGES] = {
|
||||
"GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
|
||||
"GUARD\\g3.atl", "GUARD\\g4.atl"
|
||||
};
|
||||
uint8_t i;
|
||||
for (i = 0; i < GUARD_PAGES; i++)
|
||||
if (atlas_load(&gp[i], names[i]) != 0) return -1;
|
||||
g_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_guard_free(void) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
if (!g_ok) return;
|
||||
for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]);
|
||||
g_ok = 0;
|
||||
}
|
||||
|
||||
void pop_guard_heal(void) __banked
|
||||
{
|
||||
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 (svalid[p]) gfx_heal(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]);
|
||||
gvalid[p] = svalid[p] = 0;
|
||||
}
|
||||
|
||||
void pop_guard_draw(void) __banked
|
||||
{
|
||||
int obj_x, obj_y, top, fwd, fp_x;
|
||||
uint8_t page, idx, p, flip;
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
/* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам
|
||||
* футпринт (pop_fore_over_char) считается по спрайту стража, поэтому
|
||||
* w/h ниже НЕ трогаем. */
|
||||
int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0;
|
||||
|
||||
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
|
||||
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
|
||||
fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx);
|
||||
obj_x = (fwd << 1) - 116;
|
||||
obj_y = (int)Guard.y + pop_gframe.dy;
|
||||
if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++;
|
||||
fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */
|
||||
obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */
|
||||
|
||||
page = (uint8_t)(pop_gframe.image >> 3);
|
||||
idx = (uint8_t)(pop_gframe.image & 7);
|
||||
if (page >= GUARD_PAGES) return;
|
||||
img = (const uint8_t *)atlas_image(&gp[page], idx);
|
||||
gfx_w0_map(gp[page].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
if (w && h) {
|
||||
int bx = obj_x;
|
||||
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
|
||||
if (flip) bx -= (int)w;
|
||||
top = obj_y - (int)h + 1;
|
||||
p = gfx_get_draw_page() & 1;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
|
||||
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||
else
|
||||
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
|
||||
clip_x = bx; clip_y = top; clip_w = w; clip_h = h;
|
||||
}
|
||||
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
|
||||
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
|
||||
* heal; окно fore-клипа — объединение (без него клинок полез бы поверх
|
||||
* переднего столба). */
|
||||
{
|
||||
int sx, sy; uint16_t sw, sh;
|
||||
if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y,
|
||||
&sx, &sy, &sw, &sh)) {
|
||||
p = gfx_get_draw_page() & 1;
|
||||
sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh;
|
||||
svalid[p] = 1;
|
||||
if (!clip_w) {
|
||||
clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh;
|
||||
} else {
|
||||
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
|
||||
if (sx < clip_x) clip_x = sx;
|
||||
if (sy < clip_y) clip_y = sy;
|
||||
if (sx + (int)sw > x1) x1 = sx + (int)sw;
|
||||
if (sy + (int)sh > y1) y1 = sy + (int)sh;
|
||||
clip_w = (uint16_t)(x1 - clip_x);
|
||||
clip_h = (uint16_t)(y1 - clip_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
/* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
|
||||
* fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */
|
||||
pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
|
||||
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
|
||||
* midtable, а foretable рисуется после всех midtable-записей, поэтому
|
||||
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
|
||||
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
|
||||
* столб. */
|
||||
if (w && h)
|
||||
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction);
|
||||
}
|
||||
|
||||
/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
|
||||
* Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри
|
||||
* тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража
|
||||
* справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2
|
||||
* id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы
|
||||
* просто не печатаем — полоса и так на чёрном фоне.
|
||||
*
|
||||
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
|
||||
* pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */
|
||||
#define HP_Y 194
|
||||
#define HP_STEP 7
|
||||
#define HP_KID_FULL 216
|
||||
#define HP_KID_EMPTY 217
|
||||
#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */
|
||||
|
||||
/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ
|
||||
* страницах дабл-буфера (иначе на второй останется старое значение и его
|
||||
* будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг.
|
||||
*
|
||||
* Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80
|
||||
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
||||
* sdcc_z80_cmp_store_a_bug). */
|
||||
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
||||
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
||||
|
||||
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
||||
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
||||
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
|
||||
|
||||
void pop_hp_draw(void) __banked
|
||||
{
|
||||
uint8_t i, n;
|
||||
uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0);
|
||||
uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0);
|
||||
|
||||
{ /* Изменилось хоть одно число — обновить обе страницы. */
|
||||
uint8_t k = hitp_curr, km = hitp_max;
|
||||
uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev ||
|
||||
gd != hp_gd_prev || gm != hp_gdmax_prev);
|
||||
hp_kid_prev = k; hp_kidmax_prev = km;
|
||||
hp_gd_prev = gd; hp_gdmax_prev = gm;
|
||||
if (changed) hp_todo = 2;
|
||||
}
|
||||
if (hp_todo == 0) return;
|
||||
hp_todo--;
|
||||
|
||||
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||
|
||||
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
|
||||
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
|
||||
for (i = hitp_curr; i < n; i++)
|
||||
pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF);
|
||||
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++)
|
||||
pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF);
|
||||
|
||||
/* Страж: только полные деления, справа налево. Скелет полосы не имеет. */
|
||||
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) {
|
||||
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
|
||||
uint16_t w, h;
|
||||
gfx_w0_map(gp[0].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
if (w && h) {
|
||||
/* Атлас стража тоже column-major — блит колоночный. */
|
||||
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
|
||||
for (i = 0; i < n; i++)
|
||||
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
|
||||
HP_Y + POP_YOFF, img, 0, 0, 0);
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* pop_geom.c — общая геометрия + PRNG оригинала. См. pop_geom.h.
|
||||
*/
|
||||
#include "pop_geom.h"
|
||||
|
||||
/* data.h: шаг 14 (ширина тайла в координатах персонажа), [5] = col 0. */
|
||||
const int16_t pop_x_bump[20] = {
|
||||
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
|
||||
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
|
||||
};
|
||||
const int16_t pop_y_land[5] = {-8, 55, 118, 181, 244};
|
||||
|
||||
int8_t pop_y_to_row(int16_t y)
|
||||
{
|
||||
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
|
||||
}
|
||||
|
||||
/* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
|
||||
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
|
||||
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
|
||||
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
|
||||
{
|
||||
uint16_t n = (uint16_t)(maxv + 1);
|
||||
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
|
||||
return (uint16_t)(v & (uint16_t)(n - 1));
|
||||
return (uint16_t)(v % n);
|
||||
}
|
||||
|
||||
#if POP_PRANDOM_EXACT
|
||||
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
|
||||
*
|
||||
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
|
||||
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong — 8 430
|
||||
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
|
||||
* половины это ~3 500. Здесь ~1 000, при полностью той же
|
||||
* последовательности, что в SDLPoP. Альтернатива «взять генератор
|
||||
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
|
||||
* совместимость с эталоном, по которому сверяем картинку.
|
||||
*
|
||||
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
|
||||
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
|
||||
* 0x343FD) — 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
|
||||
* нужная в конце, попадается по дороге на втором шаге — её и сохраняем.
|
||||
*
|
||||
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
|
||||
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор —
|
||||
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s — в статиках
|
||||
* (регистров на всё не хватает).
|
||||
*/
|
||||
uint16_t pop_pr_ptr; /* адрес сида */
|
||||
uint16_t pop_pr_sh; /* старшее слово исходного сида */
|
||||
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
|
||||
|
||||
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
|
||||
{
|
||||
(void)seed;
|
||||
__asm
|
||||
ld (_pop_pr_ptr), hl
|
||||
ld c, (hl)
|
||||
inc hl
|
||||
ld b, (hl) ; BC = s.lo
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
ld (_pop_pr_sh), a
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
|
||||
|
||||
ld h, b
|
||||
ld l, c
|
||||
ld de, (_pop_pr_sh) ; акк = s
|
||||
|
||||
;; акк = акк*2 + s => 3s
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, bc
|
||||
ex de, hl
|
||||
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
|
||||
adc a, l
|
||||
ld l, a
|
||||
ld a, (_pop_pr_sh + 1)
|
||||
adc a, h
|
||||
ld h, a
|
||||
ex de, hl
|
||||
ld (_pop_pr_3s_lo), hl
|
||||
ld (_pop_pr_3s_hi), de
|
||||
|
||||
;; акк = акк*4 + s => 13s
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, bc
|
||||
ex de, hl
|
||||
ld a, (_pop_pr_sh)
|
||||
adc a, l
|
||||
ld l, a
|
||||
ld a, (_pop_pr_sh + 1)
|
||||
adc a, h
|
||||
ld h, a
|
||||
ex de, hl
|
||||
|
||||
;; акк = акк*16 + s => 209s
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, bc
|
||||
ex de, hl
|
||||
ld a, (_pop_pr_sh)
|
||||
adc a, l
|
||||
ld l, a
|
||||
ld a, (_pop_pr_sh + 1)
|
||||
adc a, h
|
||||
ld h, a
|
||||
ex de, hl
|
||||
|
||||
;; акк *= 1024 => 214016s (10 удвоений)
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
|
||||
;; акк -= 3s => 214013s
|
||||
ld bc, (_pop_pr_3s_lo)
|
||||
or a ; CY = 0
|
||||
sbc hl, bc
|
||||
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
|
||||
ex de, hl
|
||||
sbc hl, bc
|
||||
ex de, hl
|
||||
|
||||
;; акк += 2531011 = 0x0026_9EC3
|
||||
ld bc, #0x9EC3
|
||||
add hl, bc
|
||||
ld bc, #0x0026
|
||||
ex de, hl
|
||||
adc hl, bc
|
||||
ex de, hl
|
||||
|
||||
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
|
||||
push de
|
||||
ex de, hl ; DE = младшее слово
|
||||
ld hl, (_pop_pr_ptr)
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
inc hl
|
||||
pop de ; DE = старшее слово
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||||
{
|
||||
return pop_rnd_fit(pop_lcg_step(seed), maxv);
|
||||
}
|
||||
|
||||
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
|
||||
|
||||
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
|
||||
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов — при двух
|
||||
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
|
||||
* умножений нет вовсе, только сдвиги и xor.
|
||||
*
|
||||
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
|
||||
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
|
||||
* младшие биты вырождены — бит 0 просто чередуется. Наши вызовы это
|
||||
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
|
||||
* вместо шума получилась бы правильная шахматка. У xorshift такой
|
||||
* структуры нет, а стоит он примерно столько же.
|
||||
*
|
||||
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
|
||||
* xorshift ноль — неподвижная точка, а сид кладки (номер комнаты + ряд +
|
||||
* колонка) вполне может оказаться нулём.
|
||||
*
|
||||
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP — другая (но
|
||||
* статистически такая же) раскладка кладки и другие броски в боёвке.
|
||||
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
|
||||
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
|
||||
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
|
||||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||||
{
|
||||
uint16_t s = seed->lo;
|
||||
s ^= (uint16_t)(s << 7);
|
||||
s ^= (uint16_t)(s >> 9);
|
||||
s ^= (uint16_t)(s << 8);
|
||||
s = (uint16_t)(s + 0x9E37u);
|
||||
seed->lo = s;
|
||||
seed->hi = 0; /* старшая половина не используется */
|
||||
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
|
||||
* младших бит, а вызывающие берут от результата именно младшие. */
|
||||
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* pop_geom.h — общая геометрия PoP и PRNG оригинала.
|
||||
*
|
||||
* Сюда сведено то, что раньше дублировалось по модулям (x_bump/y_land в
|
||||
* pop_kid и pop_map, y_to_row в pop_bg и pop_map, 32-битный LCG в pop_bg и
|
||||
* pop_trob). Кроме экономии это ФУНДАМЕНТ bank-safe API: модуль живёт в
|
||||
* W1/W2 и не трогает графику, поэтому его может звать и главный цикл, и
|
||||
* резидент W3, и будущий __banked-код стражей (docs/layout_plan_v2.md §4).
|
||||
*/
|
||||
#ifndef POP_GEOM_H
|
||||
#define POP_GEOM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define FIRST_ONSCREEN_COLUMN 5 /* индекс col 0 комнаты в x_bump */
|
||||
#define TILE_SIZEX 14 /* ширина тайла в координатах персонажа */
|
||||
#define TILE_SIZEY 63 /* высота ряда */
|
||||
|
||||
/* x_bump (data.h): левая граница колонки в координатах персонажа; колонка
|
||||
* комнаты col = x_bump[col + FIRST_ONSCREEN_COLUMN]. y_land: пол ряда
|
||||
* (индекс row+1; [0] — ряд «над комнатой»). */
|
||||
extern const int16_t pop_x_bump[20];
|
||||
extern const int16_t pop_y_land[5];
|
||||
|
||||
/* y_to_row_mod4 (seg006): ряд по координате y с оборотом mod 4 (−1 = полоса
|
||||
* над комнатой, 3 = под комнатой). */
|
||||
int8_t pop_y_to_row(int16_t y);
|
||||
|
||||
/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
|
||||
* анимаций тайлов (фаза факелов) свои последовательности, смешивать их
|
||||
* нельзя.
|
||||
*
|
||||
* ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c):
|
||||
* 1 (по умолчанию) — LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же
|
||||
* раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке
|
||||
* можно сверяться с эталоном;
|
||||
* 0 — xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на
|
||||
* 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3,
|
||||
* 175 кадров: 403 632 против 397 986). Последовательность другая —
|
||||
* статистически эквивалентная, но не та же.
|
||||
*
|
||||
* Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит
|
||||
* умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в
|
||||
* MAME, breakpoint на __mullong).
|
||||
*
|
||||
* ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией
|
||||
* SDCC — те же генераторы на Z80-ассемблере укладываются в 86..148 тактов.
|
||||
* Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там
|
||||
* умножение на константу), а менять генератор — только если и этого мало. */
|
||||
typedef struct { uint16_t lo, hi; } pop_rnd_t;
|
||||
|
||||
#ifndef POP_PRANDOM_EXACT
|
||||
#define POP_PRANDOM_EXACT 1
|
||||
#endif
|
||||
|
||||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);
|
||||
|
||||
/* Сид из 16-битного числа (старшая половина = 0). */
|
||||
#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0)
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* pop_guard.c — СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h.
|
||||
*
|
||||
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
|
||||
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
|
||||
* ОТРИСОВКА — в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл).
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "pop_guard.h"
|
||||
#include "pop_geom.h"
|
||||
#include "pop_level.h"
|
||||
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
|
||||
#include "pop_map.h" /* hitp_* — HP Кида */
|
||||
#include "kid_data.h"
|
||||
|
||||
pop_char_t Guard;
|
||||
uint8_t guardhp_curr, guardhp_max;
|
||||
int8_t guardhp_delta;
|
||||
int8_t can_guard_see_kid;
|
||||
int8_t is_guard_notice;
|
||||
uint8_t guard_skill;
|
||||
int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||
int8_t offguard, holding_sword;
|
||||
|
||||
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
|
||||
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
|
||||
* см. pop_geom.h — иначе перерисовка стены сбивала бы броски стража. */
|
||||
pop_rnd_t pop_fight_seed;
|
||||
|
||||
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
|
||||
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
|
||||
kframe pop_gframe;
|
||||
|
||||
uint8_t pop_guard_present(void)
|
||||
{
|
||||
return (uint8_t)(Guard.charid != 0 && Guard.alive < 0 && guardhp_curr != 0);
|
||||
}
|
||||
|
||||
void pop_guard_reset(void)
|
||||
{
|
||||
Guard.charid = 0;
|
||||
Guard.alive = 0;
|
||||
Guard.direction = 0;
|
||||
guardhp_curr = 0;
|
||||
guardhp_delta = 0;
|
||||
can_guard_see_kid = 0;
|
||||
is_guard_notice = 0;
|
||||
guard_refrac = justblocked = kid_sword_strike = 0;
|
||||
offguard = holding_sword = 0;
|
||||
}
|
||||
|
||||
/* kill guard cheat (seg000:786):
|
||||
* if (Guard.charid != charid_4_skeleton) {
|
||||
* guardhp_delta = -guardhp_curr;
|
||||
* Guard.alive = 0;
|
||||
* }
|
||||
* Скелета чит не убивает — он и в оригинале бессмертен. */
|
||||
void pop_guard_kill(void)
|
||||
{
|
||||
if (Guard.charid == CHARID_4_SKELETON) return;
|
||||
if (!pop_guard_present()) return;
|
||||
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
|
||||
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
|
||||
* общий control() ставит ему seq_71_dying. */
|
||||
guardhp_delta = (int8_t)-guardhp_curr;
|
||||
Guard.alive = 0;
|
||||
}
|
||||
|
||||
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
|
||||
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
|
||||
void pop_do_delta_hp(void)
|
||||
{
|
||||
int16_t hp;
|
||||
if (hitp_delta) {
|
||||
hp = (int16_t)hitp_curr + hitp_delta;
|
||||
if (hp < 0) hp = 0;
|
||||
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
|
||||
hitp_curr = (uint8_t)hp;
|
||||
pop_kid_hurt = 1; /* -> «брызги» поверх Кида */
|
||||
hitp_delta = 0;
|
||||
}
|
||||
if (guardhp_delta) {
|
||||
hp = (int16_t)guardhp_curr + guardhp_delta;
|
||||
if (hp < 0) hp = 0;
|
||||
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
|
||||
guardhp_curr = (uint8_t)hp;
|
||||
guardhp_delta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
|
||||
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где
|
||||
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||||
void pop_guard_load_frame(void)
|
||||
{
|
||||
int16_t idx = (int16_t)Guard.frame;
|
||||
if (idx >= 102 && idx < 107) idx += 70; /* add_frame */
|
||||
idx -= 149;
|
||||
if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; }
|
||||
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
|
||||
}
|
||||
|
||||
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
|
||||
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
|
||||
* последовательностью — тогда при возврате он лежит там же, а не встаёт
|
||||
* живым. */
|
||||
void pop_guard_leave(void)
|
||||
{
|
||||
if (Guard.charid == 0) return; /* стража не было */
|
||||
pop_guard_state_save(Guard.room,
|
||||
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
|
||||
Guard.direction, Guard.x, guard_skill,
|
||||
Guard.curr_seq,
|
||||
(uint8_t)(Guard.alive >= 0));
|
||||
}
|
||||
|
||||
void pop_guard_enter(uint8_t room)
|
||||
{
|
||||
uint8_t tile, color, skill;
|
||||
int8_t dir;
|
||||
pop_guard_reset();
|
||||
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
|
||||
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
|
||||
* колонки тайла, а не берётся из файла. */
|
||||
Guard.room = room;
|
||||
Guard.curr_row = (int8_t)(tile / 10);
|
||||
Guard.y = (uint8_t)pop_y_land[Guard.curr_row + 1];
|
||||
Guard.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||||
Guard.curr_col = (int8_t)(tile % 10);
|
||||
Guard.direction = dir;
|
||||
Guard.charid = CHARID_2_GUARD;
|
||||
Guard.sword = 0; /* sword_0_sheathed */
|
||||
Guard.alive = -1;
|
||||
Guard.action = 1; /* actions_1_run_jump */
|
||||
Guard.fall_x = Guard.fall_y = 0;
|
||||
/* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
|
||||
* всё, что вне диапазона, оригинал сводит к 3. */
|
||||
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
|
||||
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
|
||||
* Уровень 1 -> 3; extrastrength ненулевой только у skill 4. */
|
||||
guardhp_max = guardhp_curr = (uint8_t)(3 + (guard_skill == 4 ? 1 : 0));
|
||||
guardhp_delta = 0;
|
||||
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
|
||||
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
|
||||
* ровно там, где лёг (порт ветки guards_seq_hi != 0). */
|
||||
{
|
||||
uint16_t saved_seq = pop_guard_state_seq(room);
|
||||
/* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620)
|
||||
* пересчитывает её из колонки только при загрузке уровня, а дальше
|
||||
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
|
||||
* тайла. */
|
||||
if (saved_seq) Guard.x = pop_guard_state_x(room);
|
||||
pop_loadshad();
|
||||
if (saved_seq) Char.curr_seq = saved_seq;
|
||||
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
|
||||
pop_saveshad();
|
||||
pop_guard_play(); /* до первого кадра */
|
||||
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
|
||||
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
|
||||
Guard.alive = 1;
|
||||
guardhp_curr = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* pop_guard.h — СОСТОЯНИЕ стража и точки входа в его логику.
|
||||
*
|
||||
* Разделение по окнам (docs/layout_plan_v2.md §4, подтверждено пробником
|
||||
* tests/w3bankgfx):
|
||||
* - СОСТОЯНИЕ живёт здесь, в W1/W2. Писучие статики __banked-модуля
|
||||
* линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк —
|
||||
* только код;
|
||||
* - ЛОГИКА (ИИ, боёвка) — в guards.c, собираемом как --bank 1=guards.c;
|
||||
* - ОТРИСОВКА стража — в pop_gdraw.c, РЕЗИДЕНТЕ W3 (--w3): её зовёт
|
||||
* только главный цикл, а из банка резидент недостижим. Так код
|
||||
* отрисовки не занимает дефицитные W1/W2 (куча).
|
||||
*
|
||||
* Поля char_type — как в оригинале (types.h:302), чтобы порт seg005/seg006
|
||||
* ложился один в один.
|
||||
*/
|
||||
#ifndef POP_GUARD_H
|
||||
#define POP_GUARD_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "kid_data.h" /* kframe */
|
||||
#include "pop_char.h"
|
||||
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
|
||||
|
||||
/* seqids (types.h:1052+) — последовательности боя и стойки. */
|
||||
#define SEQ_55_DRAW_SWORD 55
|
||||
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
|
||||
#define SEQ_57_BACK_WITH_SWORD 57
|
||||
#define SEQ_58_GUARD_STRIKE 58
|
||||
#define SEQ_60_TURN_WITH_SWORD 60
|
||||
#define SEQ_61_PARRY_AFTER_STRIKE 61
|
||||
#define SEQ_62_PARRY 62
|
||||
#define SEQ_66_STRIKE_AFTER_PARRY 66
|
||||
#define SEQ_71_DYING 71
|
||||
#define SEQ_75_STRIKE 75 /* удар (Кид) */
|
||||
#define SEQ_77_GUARD_STAND_INACTIVE 77
|
||||
#define SEQ_80_STAND_FLIPPED 80
|
||||
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
|
||||
#define SEQ_87_GUARD_BECOME_INACT 87
|
||||
#define SEQ_90_EN_GARDE 90
|
||||
#define SEQ_92_PUT_SWORD_AWAY 92
|
||||
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
|
||||
|
||||
/* frameids (types.h:911+) и состояния меча (types.h:332). */
|
||||
#define FRAME_150_PARRY 150
|
||||
#define FRAME_151_STRIKE_1 151
|
||||
#define FRAME_152_STRIKE_2 152
|
||||
#define FRAME_153_STRIKE_3 153
|
||||
#define FRAME_157_WALK_WITH_SWORD 157
|
||||
#define FRAME_158_STAND_WITH_SWORD 158
|
||||
#define FRAME_161_PARRY 161
|
||||
#define FRAME_162_BLOCK_TO_STRIKE 162
|
||||
#define FRAME_165_WALK_WITH_SWORD 165
|
||||
#define FRAME_166_STAND_INACTIVE 166
|
||||
#define FRAME_167_BLOCKED 167
|
||||
#define FRAME_168_BACK 168
|
||||
#define FRAME_169_BEGIN_BLOCK 169
|
||||
#define FRAME_170_STAND_WITH_SWORD 170
|
||||
#define FRAME_171_STAND_WITH_SWORD 171
|
||||
#define SWORD_0_SHEATHED 0
|
||||
#define SWORD_2_DRAWN 2
|
||||
|
||||
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
|
||||
#define CHARID_0_KID 0
|
||||
#define CHARID_1_SHADOW 1
|
||||
#define CHARID_2_GUARD 2
|
||||
#define CHARID_4_SKELETON 4
|
||||
#define CHARID_5_PRINCESS 5
|
||||
|
||||
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
|
||||
#define NUM_GUARD_SKILLS 12
|
||||
|
||||
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
|
||||
|
||||
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
|
||||
extern uint8_t guardhp_curr, guardhp_max;
|
||||
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
|
||||
|
||||
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
|
||||
* 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра),
|
||||
* 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
|
||||
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
|
||||
extern int8_t can_guard_see_kid;
|
||||
|
||||
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
|
||||
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
|
||||
* снимает сам ИИ. */
|
||||
extern int8_t is_guard_notice;
|
||||
|
||||
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
|
||||
* кадр в autocontrol_opponent — как в оригинале.
|
||||
* guard_skill — 0..11, из данных уровня; индекс в таблицы вероятностей;
|
||||
* guard_refrac — «отдышка» стража: пока >0, он не атакует и не наступает;
|
||||
* justblocked — Кид только что парировал (влияет на вероятность блока);
|
||||
* kid_sword_strike — Кид только что бил (стража это придерживает). */
|
||||
extern uint8_t guard_skill;
|
||||
extern int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||
|
||||
/* Состояние меча КИДА (seg005/seg006):
|
||||
* offguard — меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
|
||||
* достаёт клинок автоматически, даже видя стража;
|
||||
* holding_sword — клинок в руке (для индикатора HP стража). */
|
||||
extern int8_t offguard, holding_sword;
|
||||
|
||||
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
|
||||
extern pop_rnd_t pop_fight_seed;
|
||||
|
||||
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
|
||||
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
|
||||
void pop_check_can_guard_see_kid(void) __banked;
|
||||
|
||||
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
|
||||
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
|
||||
void pop_check_sword_hurting(void) __banked;
|
||||
void pop_check_sword_hurt(void) __banked;
|
||||
|
||||
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
|
||||
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
||||
extern kframe pop_gframe;
|
||||
void pop_guard_load_frame(void);
|
||||
|
||||
/* Есть ли живой страж в отрисованной комнате (для главного цикла). */
|
||||
uint8_t pop_guard_present(void);
|
||||
|
||||
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
|
||||
* 0 — OK, -1 — ошибка (тогда страж просто не рисуется). */
|
||||
int pop_guard_load(void) __banked;
|
||||
void pop_guard_free(void) __banked;
|
||||
|
||||
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
|
||||
* seg002:0112) либо убрать его, если в комнате стража нет. */
|
||||
void pop_guard_enter(uint8_t room);
|
||||
|
||||
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
|
||||
* enter_room, пока Guard ещё держит прежнего. */
|
||||
void pop_guard_leave(void);
|
||||
|
||||
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
|
||||
* как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c
|
||||
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
|
||||
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
|
||||
* ПОСЛЕ персонажей — она ничего не перекрывает. */
|
||||
void pop_hp_draw(void) __banked;
|
||||
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
|
||||
void pop_hp_invalidate(void) __banked;
|
||||
|
||||
void pop_guard_heal(void) __banked;
|
||||
void pop_guard_draw(void) __banked;
|
||||
|
||||
/* Убрать стража со сцены (смена комнаты / инициализация). */
|
||||
void pop_guard_reset(void);
|
||||
|
||||
/* Чит «убить стража» (seg000:786, K): скелета не берёт. Вызывать только
|
||||
* при включённых читах — проверку делает вызывающий. */
|
||||
void pop_guard_kill(void);
|
||||
|
||||
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
|
||||
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */
|
||||
void pop_do_delta_hp(void);
|
||||
|
||||
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
|
||||
void pop_guard_tick(void) __banked;
|
||||
|
||||
#endif
|
||||
@@ -2,36 +2,125 @@
|
||||
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
|
||||
* seqtbl-интерпретатор. Данные — kid_data.h.
|
||||
*/
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_kid.h"
|
||||
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
|
||||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||||
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
||||
#include "pop_geom.h"
|
||||
#include "kid_data.h"
|
||||
|
||||
kid_t Kid;
|
||||
pop_char_t Kid;
|
||||
|
||||
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
|
||||
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
||||
int8_t knock;
|
||||
|
||||
/* Координатные таблицы (data.h). */
|
||||
static const uint8_t x_bump[20] = {
|
||||
(uint8_t)-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
|
||||
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
|
||||
};
|
||||
static const int16_t y_land[5] = {-8, 55, 118, 181, 244};
|
||||
#define FIRST_ONSCREEN_COLUMN 5
|
||||
#define TILE_SIZEX 14
|
||||
|
||||
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
|
||||
#define KID_MAXPAGES 28
|
||||
static atlas_t kidp[KID_MAXPAGES];
|
||||
static uint8_t kid_npages;
|
||||
|
||||
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
|
||||
* ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
|
||||
* Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
|
||||
* sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
|
||||
* Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
|
||||
* calc_screen_x_coord, x — «вперёд» по направлению персонажа. */
|
||||
static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
|
||||
static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
|
||||
static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
|
||||
static atlas_t swordp;
|
||||
static uint8_t sword_ok;
|
||||
|
||||
int pop_sword_load(void)
|
||||
{
|
||||
if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
|
||||
sword_ok = 1;
|
||||
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 выдал бы марку транспонированной. */
|
||||
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
int pop_kid_load(uint8_t npages)
|
||||
{
|
||||
static const char *const names[KID_MAXPAGES] = {
|
||||
"kid0.atl","kid1.atl","kid2.atl","kid3.atl","kid4.atl","kid5.atl",
|
||||
"kid6.atl","kid7.atl","kid8.atl","kid9.atl","kid10.atl","kid11.atl",
|
||||
"kid12.atl","kid13.atl","kid14.atl","kid15.atl","kid16.atl","kid17.atl",
|
||||
"kid18.atl","kid19.atl","kid20.atl","kid21.atl","kid22.atl","kid23.atl",
|
||||
"kid24.atl","kid25.atl","kid26.atl","kid27.atl"
|
||||
"KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
|
||||
"KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
|
||||
"KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
|
||||
"KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
|
||||
"KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
|
||||
};
|
||||
uint8_t i;
|
||||
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
|
||||
@@ -50,99 +139,325 @@ void pop_kid_free(void)
|
||||
kid_npages = 0;
|
||||
}
|
||||
|
||||
/* ---- seqtbl-доступ (база SEQTBL_BASE) ------------------------------ */
|
||||
#define SEQ(cs) kid_seqtbl[(uint16_t)((cs) - SEQTBL_BASE)]
|
||||
/* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
|
||||
* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
|
||||
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
|
||||
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
|
||||
* весь остальной код читает cur_frame — 5-байтовую копию текущего кадра.
|
||||
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
|
||||
* и коллизия/отрисовка работают с ним, а не с таблицей.
|
||||
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
||||
extern void _gfx_w0_isr(void);
|
||||
|
||||
#define KD_DATA_OFF 0x100u
|
||||
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
|
||||
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
|
||||
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
|
||||
* берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
|
||||
* (`ld c,l` / `inc b`). Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
|
||||
* −256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
|
||||
* пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
|
||||
* Артефакт: .sprinter-cc-roomtest/pop_kid.asm, функция load_frame. */
|
||||
static const uint8_t *kd_frame_ptr(uint8_t i)
|
||||
{
|
||||
uint16_t off = (uint16_t)i;
|
||||
off += (uint16_t)(off << 2); /* i*5 = i + i*4, без умножения */
|
||||
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
|
||||
return (const uint8_t *)off;
|
||||
}
|
||||
|
||||
/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
|
||||
* страницу данных, см. pop_extract_kid_data.py). */
|
||||
static const uint8_t *kd_gframe_ptr(uint8_t i)
|
||||
{
|
||||
uint16_t off = (uint16_t)i;
|
||||
off += (uint16_t)(off << 2);
|
||||
off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
|
||||
return (const uint8_t *)off;
|
||||
}
|
||||
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
|
||||
(uint16_t)((cs) - SEQTBL_BASE)))
|
||||
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
|
||||
* страницей данных — то есть внутри play_seq. */
|
||||
#define SEQ(cs) KD_SEQ(cs)
|
||||
|
||||
static uint8_t kdat_blk, kdat_page, kdat_ok;
|
||||
|
||||
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
|
||||
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
|
||||
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
|
||||
* тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
|
||||
static kframe cur_frame;
|
||||
static kframe kid_frame; /* кадр Kid для kid_draw */
|
||||
|
||||
/* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в
|
||||
* оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
|
||||
* стоят loadkid/savekid и loadshad/saveshad (seg006:809..825). Копия, а
|
||||
* не указатель, — и так же, как в оригинале, и быстрее на Z80: обращения
|
||||
* в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
|
||||
* копирование платится один раз на переключение персонажа. */
|
||||
pop_char_t Char;
|
||||
|
||||
/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
|
||||
* рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
|
||||
* ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
|
||||
* ними: для стража Opp = Kid, для Кида Opp = Guard. */
|
||||
pop_char_t Opp;
|
||||
|
||||
/* Снимок последовательности на момент входа в окно Char: по нему
|
||||
* pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)»
|
||||
* от «её сменил pop_map (прямо в Kid)». */
|
||||
static uint16_t kid_seq_at_load;
|
||||
|
||||
void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; }
|
||||
void pop_savekid(void) { Kid = Char; }
|
||||
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально
|
||||
* меняет у активного персонажа — последовательность и состояние меча.
|
||||
*
|
||||
* Почему не `Kid = Char`, как делает savekid оригинала: у нас часть
|
||||
* действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq,
|
||||
* safe_step...), и он пишет ПРЯМО в Kid — там ещё не портирован переход на
|
||||
* Char. Полное копирование затирало эти правки: Kid терял выравнивание x
|
||||
* и ряд, и спуск с уступа через вис не срабатывал — персонаж просто
|
||||
* приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */
|
||||
void pop_savekid_state(void)
|
||||
{
|
||||
/* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char
|
||||
* (pop_char_set_seq), pop_map — прямо в Kid (kid_set_seq: спуск с
|
||||
* уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше:
|
||||
* диспетчер про эти переходы не знает и, копируя Char вслепую, затирал
|
||||
* их — спуск в проём не срабатывал, а персонаж даже не приседал, потому
|
||||
* что pop_down_action уже вернул «обработано». */
|
||||
uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load);
|
||||
if (!map_changed) Kid.curr_seq = Char.curr_seq;
|
||||
Kid.sword = Char.sword;
|
||||
}
|
||||
void pop_loadshad(void) { Char = Guard; }
|
||||
void pop_saveshad(void) { Guard = Char; }
|
||||
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
|
||||
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
|
||||
void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; }
|
||||
void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; }
|
||||
|
||||
/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
|
||||
* +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
|
||||
* приходится на ширину тайла между ними. Живёт здесь, а не в guards.c,
|
||||
* потому что нужен по обе стороны банковой границы — и ИИ, и диспетчеру
|
||||
* боёвки (pop_ctrl). */
|
||||
int16_t pop_char_opp_dist(void)
|
||||
{
|
||||
int16_t distance;
|
||||
if (Char.room != Opp.room) return 999;
|
||||
distance = (int16_t)Opp.x - (int16_t)Char.x;
|
||||
if (Char.direction < 0) distance = -distance;
|
||||
if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
|
||||
return distance;
|
||||
}
|
||||
|
||||
int pop_kid_data_load(const char *path)
|
||||
{
|
||||
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
||||
uint8_t saved_w3, blk;
|
||||
uint16_t stub;
|
||||
int fd, n;
|
||||
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
blk = mem_alloc_pages(1);
|
||||
if (!blk) { close(fd); return -1; }
|
||||
kdat_blk = blk;
|
||||
kdat_page = mem_get_page(blk, 0);
|
||||
|
||||
saved_w3 = _io_page_w3;
|
||||
sprinter_page_w3(kdat_page);
|
||||
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
|
||||
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
|
||||
pg[0x38] = 0xC3;
|
||||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
||||
pg[0x3A] = (uint8_t)(stub >> 8);
|
||||
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
|
||||
}
|
||||
sprinter_page_w3(saved_w3);
|
||||
close(fd);
|
||||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
|
||||
kdat_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_kid_data_free(void)
|
||||
{
|
||||
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
|
||||
}
|
||||
|
||||
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
|
||||
* разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF). Сама маппит страницу,
|
||||
* поэтому годится для вызова извне (pop_guard) — но НЕ в горячем цикле. */
|
||||
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
|
||||
{
|
||||
const uint8_t *f;
|
||||
uint16_t off = (uint16_t)idx;
|
||||
if (!kdat_ok) { out5[0] = 255; return; }
|
||||
off += (uint16_t)(off << 2); /* idx*5, без умножения */
|
||||
off += (uint16_t)(KD_DATA_OFF + tbl_off);
|
||||
gfx_w0_map(kdat_page);
|
||||
f = (const uint8_t *)off;
|
||||
out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
|
||||
out5[3] = f[3]; out5[4] = f[4];
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
|
||||
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
|
||||
static void load_frame(void)
|
||||
{
|
||||
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
|
||||
* стража frame_tbl_guard с индексом frame + add_frame − 149, где
|
||||
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||||
const uint8_t *f;
|
||||
if (Char.charid == CHARID_2_GUARD) {
|
||||
int16_t idx = (int16_t)Char.frame;
|
||||
if (idx >= 102 && idx < 107) idx += 70;
|
||||
idx -= 149;
|
||||
if (idx < 0 || idx >= KID_NGFRAMES) { cur_frame.image = 255; return; }
|
||||
f = kd_gframe_ptr((uint8_t)idx);
|
||||
} else {
|
||||
f = kd_frame_ptr(Char.frame);
|
||||
}
|
||||
cur_frame.image = f[0];
|
||||
cur_frame.dx = (int8_t)f[1];
|
||||
cur_frame.dy = (int8_t)f[2];
|
||||
cur_frame.flags = f[3];
|
||||
cur_frame.sword = f[4];
|
||||
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
|
||||
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
|
||||
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
|
||||
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
|
||||
* на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа
|
||||
* Кида и рисовал произвольную позу. Теперь владелец один — тот, для
|
||||
* кого кадр реально загружен. */
|
||||
if (Char.charid == CHARID_0_KID) kid_frame = cur_frame;
|
||||
else pop_gframe = cur_frame;
|
||||
}
|
||||
|
||||
static uint8_t char_dx_forward(int8_t dx)
|
||||
{
|
||||
if (Kid.direction < 0) dx = (int8_t)-dx;
|
||||
return (uint8_t)(Kid.x + dx);
|
||||
if (Char.direction < 0) dx = (int8_t)-dx;
|
||||
return (uint8_t)(Char.x + dx);
|
||||
}
|
||||
|
||||
/* Аксессоры к текущему кадру (для pop_map — без дублирования kid_frames). */
|
||||
int8_t kid_cur_dx(void) { return kid_frames[Kid.frame].dx; }
|
||||
uint8_t kid_cur_flags(void) { return kid_frames[Kid.frame].flags; }
|
||||
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
|
||||
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
|
||||
int8_t kid_cur_dx(void) { return cur_frame.dx; }
|
||||
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
|
||||
|
||||
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
|
||||
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
|
||||
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
|
||||
|
||||
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0). */
|
||||
/* То же для АКТИВНОГО персонажа — внутри окна Char (порт
|
||||
* seqtbl_offset_char, seg006). Нужен стражу: его последовательность
|
||||
* ставится, когда Char уже загружен из Guard. */
|
||||
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }
|
||||
|
||||
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
|
||||
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
|
||||
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
|
||||
* load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
|
||||
void play_seq(void)
|
||||
{
|
||||
if (!kdat_ok) return;
|
||||
gfx_w0_map(kdat_page);
|
||||
for (;;) {
|
||||
uint8_t cmd = SEQ(Kid.curr_seq);
|
||||
Kid.curr_seq++;
|
||||
uint8_t cmd = SEQ(Char.curr_seq);
|
||||
Char.curr_seq++;
|
||||
switch (cmd) {
|
||||
case 0xFB: /* DX */
|
||||
Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq));
|
||||
Kid.curr_seq++;
|
||||
Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xFA: /* DY */
|
||||
Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq));
|
||||
Kid.curr_seq++;
|
||||
Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xFE: /* FLIP */
|
||||
Kid.direction = (int8_t)~Kid.direction;
|
||||
Char.direction = (int8_t)~Char.direction;
|
||||
break;
|
||||
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
|
||||
Kid.curr_seq += 2;
|
||||
Char.curr_seq += 2;
|
||||
break;
|
||||
case 0xFF: { /* JMP */
|
||||
uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) |
|
||||
((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8));
|
||||
Kid.curr_seq = a;
|
||||
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
|
||||
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
|
||||
Char.curr_seq = a;
|
||||
break;
|
||||
}
|
||||
case 0xFD: /* UP */
|
||||
Kid.curr_row--;
|
||||
Char.curr_row--;
|
||||
break;
|
||||
case 0xFC: /* DOWN */
|
||||
Kid.curr_row++;
|
||||
Char.curr_row++;
|
||||
break;
|
||||
case 0xF9: /* ACTION */
|
||||
Kid.action = SEQ(Kid.curr_seq);
|
||||
Kid.curr_seq++;
|
||||
Char.action = SEQ(Char.curr_seq);
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xF8: /* SET_FALL */
|
||||
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
|
||||
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
|
||||
Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||||
Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||||
break;
|
||||
case 0xF5: case 0xF4: /* KNOCK up/down */
|
||||
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
|
||||
knock = 1;
|
||||
break;
|
||||
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
|
||||
knock = -1;
|
||||
break;
|
||||
case 0xF2: /* SOUND — потребить 1 байт */
|
||||
Kid.curr_seq++;
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xF1: /* END_LEVEL */
|
||||
break;
|
||||
case 0xF3: /* GET_ITEM — потребить 1 байт */
|
||||
Kid.curr_seq++;
|
||||
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
|
||||
* (порт play_seq -> proc_get_object, seg006) */
|
||||
if (SEQ(Char.curr_seq) == 1)
|
||||
pop_proc_get_object();
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xF6: /* DIE nop */
|
||||
break;
|
||||
default: /* КАДР */
|
||||
Kid.frame = cmd;
|
||||
Char.frame = cmd;
|
||||
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
|
||||
gfx_w0_unmap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
|
||||
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
|
||||
* работает с Char и сам по себе не знает, чей это персонаж. */
|
||||
void pop_kid_play(void) { pop_loadkid(); play_seq(); pop_savekid(); }
|
||||
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }
|
||||
|
||||
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
|
||||
{
|
||||
Kid.curr_col = col;
|
||||
Kid.curr_row = row;
|
||||
Kid.direction = dir;
|
||||
Kid.x = (uint8_t)(x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||||
Kid.y = (uint8_t)y_land[row + 1];
|
||||
Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||||
Kid.y = (uint8_t)pop_y_land[row + 1];
|
||||
Kid.fall_x = Kid.fall_y = 0;
|
||||
Kid.repeat = 0;
|
||||
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
|
||||
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
|
||||
Kid.curr_seq = kid_seq_off[seq_id];
|
||||
play_seq(); /* до первого кадра */
|
||||
pop_kid_play(); /* до первого кадра */
|
||||
}
|
||||
|
||||
void kid_tick(void)
|
||||
{
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
}
|
||||
|
||||
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
|
||||
@@ -172,35 +487,153 @@ static int kid_lx[2], kid_ly[2];
|
||||
static uint16_t kid_lw[2], kid_lh[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):
|
||||
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
|
||||
static int kid_fp_x, kid_fp_y;
|
||||
static uint16_t kid_fp_w, kid_fp_h;
|
||||
/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room):
|
||||
* ∓140 когда комната Kid ≠ отрисованной; 0 обычно. */
|
||||
static int kid_render_dx;
|
||||
void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; }
|
||||
int kid_fp_obj_x(void) { return kid_fp_x; }
|
||||
int kid_fp_obj_y(void) { return kid_fp_y; }
|
||||
uint16_t kid_fp_width(void) { return kid_fp_w; }
|
||||
uint16_t kid_fp_height(void) { return kid_fp_h; }
|
||||
|
||||
/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт
|
||||
* chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27).
|
||||
* Позиция от кадра Kid: obj_y −= 15, obj_x += 5 ВПЕРЁД (obj_dx_forward).
|
||||
* Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают
|
||||
* иначе — тут только общий случай + «на пиках» (порт по мере надобности).
|
||||
* Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */
|
||||
#define KID_SPLASH_IMAGE 218
|
||||
|
||||
void kid_draw_splash(void)
|
||||
{
|
||||
int obj_x = kid_fp_x, obj_y = kid_fp_y - 15;
|
||||
uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7;
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
if (page >= kid_npages) return;
|
||||
if (Kid.frame == 177) /* напоролся на пики */
|
||||
obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5;
|
||||
else
|
||||
obj_x += (Kid.direction < 0) ? -5 : 5;
|
||||
obj_x = obj_x * 8 / 7; /* лог. X -> экран (calc_screen_x_coord) */
|
||||
img = (const uint8_t *)atlas_image(&kidp[page], idx);
|
||||
gfx_w0_map(kidp[page].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
if (w && h) {
|
||||
uint8_t flip = (Kid.direction >= 0);
|
||||
int bx = obj_x;
|
||||
int top = obj_y - (int)h + 1;
|
||||
if (flip) bx -= (int)w;
|
||||
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_set_bank(GFX_BANK_NORMAL);
|
||||
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
|
||||
/* Брызги — накладной спрайт: свой прямоугольник heal (см.
|
||||
* kid_overlay_add), окно fore пересчитываем объединением. */
|
||||
{
|
||||
uint8_t dp = gfx_get_draw_page();
|
||||
kid_overlay_add(dp, bx, top, w, h);
|
||||
kid_fore_clip(dp);
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
|
||||
void kid_heal(void)
|
||||
{
|
||||
uint8_t p = gfx_get_draw_page();
|
||||
if (kid_lvalid[p])
|
||||
gfx_heal(kid_lx[p], kid_ly[p], kid_lw[p], kid_lh[p]);
|
||||
kid_lvalid[p] = 0;
|
||||
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
|
||||
if (kid_ovalid[p])
|
||||
gfx_heal(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)
|
||||
{
|
||||
kframe fr = kid_frames[Kid.frame];
|
||||
kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */
|
||||
int fwd, obj_x, obj_y, top;
|
||||
uint8_t page, idx;
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
|
||||
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
|
||||
* кадра / страница не загружена) перекрывать нечего, и fore-проход
|
||||
* не должен трогать экран. */
|
||||
pop_fore_set_clip(0, 0, 0, 0);
|
||||
if (fr.image == 255)
|
||||
return; /* пустой кадр */
|
||||
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx);
|
||||
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
|
||||
* отрисованной — спрайт за кромкой. Коллизия (Kid.x в pop_map) НЕ
|
||||
* смещается: она в кадре комнаты Kid; смещение только рендерное. */
|
||||
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx;
|
||||
obj_x = (fwd << 1) - 116;
|
||||
obj_y = (int)Kid.y + fr.dy;
|
||||
if ((int8_t)(fr.flags ^ Kid.direction) >= 0)
|
||||
@@ -224,16 +657,51 @@ void kid_draw(void)
|
||||
if (w && h) {
|
||||
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
|
||||
int bx = obj_x + KID_XOFF;
|
||||
int skip = 0; /* строк спрайта срезано сверху */
|
||||
int ct;
|
||||
if (flip)
|
||||
bx -= (int)w; /* seg008: xpos -= image->w */
|
||||
top = obj_y - (int)h + 1 + KID_YOFF;
|
||||
uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */
|
||||
/* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше
|
||||
* линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает
|
||||
* сквозь щели кладки при спуске/висе. */
|
||||
ct = pop_clip_char_top(kid_fp_x, obj_y, w, h);
|
||||
if (ct > top) {
|
||||
skip = ct - top;
|
||||
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
|
||||
}
|
||||
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
||||
gfx_blit_cols(bx, top, img, flip);
|
||||
if (skip < (int)h) { /* +YOFF: центрирование */
|
||||
if (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(bx, top + skip + POP_YOFF, img, flip, skip, 0);
|
||||
}
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
kid_lx[dp] = bx; kid_ly[dp] = top;
|
||||
kid_lw[dp] = w; kid_lh[dp] = h; kid_lvalid[dp] = 1;
|
||||
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
|
||||
* узко, только по прямоугольнику Кида и только при выходе. */
|
||||
pop_clip_sprite(bx, w, top + skip + POP_YOFF, (int)h - skip);
|
||||
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
|
||||
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
|
||||
kid_lx[dp] = bx; kid_ly[dp] = top + skip;
|
||||
kid_lw[dp] = w; kid_lh[dp] = (uint16_t)((int)h - skip);
|
||||
kid_lvalid[dp] = (skip < (int)h);
|
||||
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
|
||||
}
|
||||
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
|
||||
* Kid — со СВОИМ прямоугольником heal (kid_overlay_add). */
|
||||
{
|
||||
int sx, sy; uint16_t sw, sh;
|
||||
if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
|
||||
kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
/* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
|
||||
* fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
|
||||
* тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
|
||||
* нулевое окно = fore-проход не трогает экран, что и правильно. */
|
||||
kid_fore_clip(gfx_get_draw_page() & 1);
|
||||
}
|
||||
|
||||
@@ -11,26 +11,48 @@
|
||||
#define POP_KID_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_char.h"
|
||||
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
|
||||
|
||||
/* Состояние персонажа (подмножество char_type, types.h). */
|
||||
typedef struct {
|
||||
uint8_t frame;
|
||||
uint8_t x, y;
|
||||
int8_t direction; /* -1 влево, 0 вправо */
|
||||
int8_t curr_col, curr_row;
|
||||
uint8_t action;
|
||||
int8_t fall_x, fall_y;
|
||||
uint8_t repeat;
|
||||
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
|
||||
} kid_t;
|
||||
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
|
||||
extern pop_char_t Kid;
|
||||
|
||||
extern kid_t Kid;
|
||||
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
|
||||
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
|
||||
extern int8_t knock;
|
||||
|
||||
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
||||
* 0 — OK, -1 — ошибка. */
|
||||
int pop_kid_load(uint8_t npages);
|
||||
|
||||
/* Данные анимации (таблица кадров + seqtbl) — отдельная EMM-страница
|
||||
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
|
||||
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
|
||||
* Страница маппится в W0 внутри play_seq, раз за тик. */
|
||||
int pop_kid_data_load(const char *path);
|
||||
|
||||
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
|
||||
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
|
||||
* Маппит страницу сама; для горячего цикла есть cur_frame. */
|
||||
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5);
|
||||
void pop_kid_data_free(void);
|
||||
|
||||
/* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах
|
||||
* подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798).
|
||||
* Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда
|
||||
* кадры рисуются без клинка). */
|
||||
int pop_sword_load(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,
|
||||
* направление 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);
|
||||
@@ -41,7 +63,34 @@ void kid_tick(void);
|
||||
|
||||
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
|
||||
* physics-шаг, как оригинал). */
|
||||
void play_seq(void);
|
||||
void play_seq(void); /* работает с Char — см. pop_kid_play ниже */
|
||||
|
||||
/* Активный персонаж (порт Char, seg006) и его загрузка/выгрузка.
|
||||
* Интерпретатор последовательностей общий для Kid и стража: у них общая
|
||||
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
|
||||
extern pop_char_t Char;
|
||||
/* Соперник активного персонажа (порт Opp): ставится вместе с Char через
|
||||
* pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */
|
||||
extern pop_char_t Opp;
|
||||
void pop_loadkid(void);
|
||||
void pop_savekid(void);
|
||||
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
|
||||
void pop_loadshad(void);
|
||||
void pop_saveshad(void);
|
||||
void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */
|
||||
void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */
|
||||
void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */
|
||||
void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */
|
||||
|
||||
/* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной,
|
||||
* 999 — разные комнаты (порт char_opp_dist, seg006:2135). */
|
||||
int16_t pop_char_opp_dist(void);
|
||||
|
||||
/* Прокрутить последовательность персонажа целиком (load + play_seq +
|
||||
* save). Звать вместо голого play_seq. */
|
||||
void pop_char_set_seq(uint8_t seq_id); /* seq активного персонажа */
|
||||
void pop_kid_play(void);
|
||||
void pop_guard_play(void);
|
||||
|
||||
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
|
||||
* таблицы во втором TU). */
|
||||
@@ -52,10 +101,26 @@ uint8_t kid_cur_flags(void);
|
||||
* точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */
|
||||
void kid_set_seq(uint8_t seq_id);
|
||||
|
||||
/* Рендер-смещение Kid по ЛОГИЧЕСКОЙ X (порт xpos_in_drawn_room, seg004):
|
||||
* когда комната Kid ≠ отрисованной (straddle у шва), спрайт рисуется со
|
||||
* сдвигом ∓140 (левый сосед −140 / правый +140), при этом Kid.x/коллизия
|
||||
* остаются в кадре комнаты Kid. 0 — комнаты совпадают (обычный случай).
|
||||
* Влияет на kid_draw и метрики футпринта (fore) — heal берёт запомненный
|
||||
* прямоугольник, поэтому согласован автоматически. */
|
||||
void pop_kid_set_render_dx(int dx);
|
||||
|
||||
/* Нарисовать текущий кадр Kid (банк GFX_BANK_SPRITE — видео-ОЗУ, без
|
||||
* теневой копии; запоминает прямоугольник для kid_heal). */
|
||||
void kid_draw(void);
|
||||
|
||||
/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid —
|
||||
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
|
||||
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_tick/kid_draw. */
|
||||
void kid_heal(void);
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* pop_level.c — загрузка уровня PoP из сырого res200N.bin (Фаза L1).
|
||||
* См. pop_level.h. Порт формата blueprnt DAT 1.0 (Table 6 POP-DAT).
|
||||
*/
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
|
||||
#include "pop_level.h"
|
||||
|
||||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
||||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
||||
* _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но
|
||||
* стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
|
||||
extern void _gfx_w0_isr(void);
|
||||
|
||||
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */
|
||||
|
||||
/* Смещения блоков blueprnt относительно начала данных (Table 6). */
|
||||
#define BP_FG 0u
|
||||
#define BP_BG 720u
|
||||
#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */
|
||||
#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */
|
||||
#define BP_LINKS 1952u
|
||||
/* Стражи (level_type, types.h:228): по байту на комнату в каждом массиве. */
|
||||
#define BP_GTILE 2119u
|
||||
#define BP_GDIR 2143u
|
||||
#define BP_GX 2167u
|
||||
#define BP_GSEQLO 2191u
|
||||
#define BP_GSKILL 2215u
|
||||
#define BP_GSEQHI 2239u
|
||||
#define BP_GCOLOR 2263u
|
||||
#define BP_START 2112u
|
||||
#define ROOM_TILES 30u
|
||||
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
|
||||
|
||||
/* fwd: инициализация живой копии стражей (тело ниже). */
|
||||
static void gstate_init(void);
|
||||
|
||||
|
||||
static uint8_t lvl_blk, lvl_page, lvl_ok;
|
||||
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
|
||||
static uint8_t dl1[256], dl2[256];
|
||||
|
||||
int pop_level_load(const char *path)
|
||||
{
|
||||
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
||||
uint8_t saved_w3, blk;
|
||||
uint16_t stub;
|
||||
int fd, n;
|
||||
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
blk = mem_alloc_pages(1);
|
||||
if (!blk) { close(fd); return -1; }
|
||||
lvl_blk = blk;
|
||||
lvl_page = mem_get_page(blk, 0);
|
||||
|
||||
saved_w3 = _io_page_w3;
|
||||
sprinter_page_w3(lvl_page);
|
||||
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
|
||||
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
|
||||
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
|
||||
uint16_t i;
|
||||
stub = (uint16_t)&_gfx_w0_isr;
|
||||
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
|
||||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
||||
pg[0x3A] = (uint8_t)(stub >> 8);
|
||||
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
|
||||
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
|
||||
for (i = 0; i < 256; i++) {
|
||||
dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
|
||||
dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
|
||||
}
|
||||
}
|
||||
sprinter_page_w3(saved_w3);
|
||||
close(fd);
|
||||
|
||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||
lvl_ok = 1;
|
||||
gstate_init(); /* живая копия состояния стражей — из уровня */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_level_free(void)
|
||||
{
|
||||
if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; }
|
||||
}
|
||||
|
||||
/* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
|
||||
* замаплена вызывающим). */
|
||||
static const uint8_t *room_fg_ptr(uint8_t r)
|
||||
{
|
||||
return (const uint8_t *)(LVL_DATA_OFF + BP_FG) + (uint16_t)(r - 1) * ROOM_TILES;
|
||||
}
|
||||
static const uint8_t *room_bg_ptr(uint8_t r)
|
||||
{
|
||||
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
|
||||
}
|
||||
|
||||
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
|
||||
uint8_t *lcol_fg, uint8_t *lcol_bg,
|
||||
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg)
|
||||
{
|
||||
const uint8_t *rfg, *rbg, *lnk;
|
||||
uint8_t left, right, down, i;
|
||||
|
||||
if (!lvl_ok || room < 1 || room > 24) return -1;
|
||||
|
||||
gfx_w0_map(lvl_page);
|
||||
rfg = room_fg_ptr(room);
|
||||
rbg = room_bg_ptr(room);
|
||||
lnk = (const uint8_t *)(LVL_DATA_OFF + BP_LINKS) + (uint16_t)(room - 1) * 4;
|
||||
|
||||
for (i = 0; i < ROOM_TILES; i++) {
|
||||
fg[i] = (uint8_t)(rfg[i] & 0x1F); /* тип тайла (верх. биты — модиф., пока не нужны) */
|
||||
bg[i] = rbg[i]; /* backtable как есть */
|
||||
}
|
||||
|
||||
left = lnk[0]; /* L */
|
||||
right = lnk[1]; /* R */
|
||||
down = lnk[3]; /* D */
|
||||
|
||||
/* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0),
|
||||
* а в [3..5] — ПРЕДпоследний (col8). Вторая колонка нужна коллизии: Kid,
|
||||
* стоящий В шве (curr_col = −1), смотрит вперёд на колонку −2, и без неё
|
||||
* там оказывалась мнимая стена (get_tile → TILE_WALL), которая выпихивала
|
||||
* его сквозь решётку. Оригинал резолвит любую колонку через
|
||||
* find_room_of_tile (seg006:005D). */
|
||||
if (left >= 1 && left <= 24) {
|
||||
const uint8_t *lf = room_fg_ptr(left);
|
||||
const uint8_t *lb = room_bg_ptr(left);
|
||||
for (i = 0; i < 3; i++) {
|
||||
lcol_fg[i] = (uint8_t)(lf[i * 10 + 9] & 0x1F);
|
||||
lcol_fg[3 + i] = (uint8_t)(lf[i * 10 + 8] & 0x1F);
|
||||
lcol_bg[i] = lb[i * 10 + 9];
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_fg[3 + i] = 20; lcol_bg[i] = 0; }
|
||||
}
|
||||
|
||||
/* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9:
|
||||
* коллизия на правом шве + переход вправо) */
|
||||
if (right >= 1 && right <= 24) {
|
||||
const uint8_t *rf = room_fg_ptr(right);
|
||||
const uint8_t *rb = room_bg_ptr(right);
|
||||
for (i = 0; i < 3; i++) {
|
||||
rcol_fg[i] = (uint8_t)(rf[i * 10 + 0] & 0x1F);
|
||||
rcol_fg[3 + i] = (uint8_t)(rf[i * 10 + 1] & 0x1F); /* колонка 11 (см. выше) */
|
||||
rcol_bg[i] = rb[i * 10 + 0];
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
|
||||
}
|
||||
|
||||
/* belowrow: верхний ряд (row0) комнаты снизу */
|
||||
if (down >= 1 && down <= 24) {
|
||||
const uint8_t *df = room_fg_ptr(down);
|
||||
for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F);
|
||||
} else {
|
||||
for (i = 0; i < 10; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */
|
||||
}
|
||||
|
||||
gfx_w0_unmap();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t pop_room_col_landing(uint8_t room, uint8_t col)
|
||||
{
|
||||
const uint8_t *rf;
|
||||
int8_t row, res = -1;
|
||||
if (!lvl_ok || room < 1 || room > 24 || col > 9) return -1;
|
||||
gfx_w0_map(lvl_page);
|
||||
rf = room_fg_ptr(room);
|
||||
for (row = 0; row < 3; row++) { /* кусок пролетает пустые ряды */
|
||||
uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
|
||||
if (t == 0) continue; /* empty — дальше вниз */
|
||||
if (t == 1) res = row; /* floor — приземление → debris */
|
||||
break; /* первый не-пустой тайл — стоп */
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
return res;
|
||||
}
|
||||
|
||||
uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; }
|
||||
uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; }
|
||||
void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }
|
||||
|
||||
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos)
|
||||
{
|
||||
uint8_t v;
|
||||
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
|
||||
gfx_w0_map(lvl_page);
|
||||
v = (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
|
||||
gfx_w0_unmap();
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Батч-доступ: смаппить страницу уровня в W0 ОДИН раз, прочитать много
|
||||
* тайлов через pop_level_tile_raw, размаппить один раз. Экономит N свапов
|
||||
* окна 0 в горячих циклах (pop_process_trobs звал pop_level_tile на каждый
|
||||
* trob каждый кадр). Между begin/end НЕЛЬЗЯ рисовать/звать код, которому
|
||||
* нужно окно 0 в исходном состоянии — только сырые чтения уровня. */
|
||||
void pop_level_access_begin(void) { if (lvl_ok) gfx_w0_map(lvl_page); }
|
||||
void pop_level_access_end(void) { if (lvl_ok) gfx_w0_unmap(); }
|
||||
|
||||
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
|
||||
{
|
||||
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
|
||||
return (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
|
||||
}
|
||||
|
||||
void pop_level_room_bg(uint8_t room, uint8_t *out30)
|
||||
{
|
||||
const uint8_t *rbg;
|
||||
uint8_t i;
|
||||
if (!out30) return;
|
||||
if (!lvl_ok || room < 1 || room > 24) {
|
||||
for (i = 0; i < ROOM_TILES; i++) out30[i] = 0;
|
||||
return;
|
||||
}
|
||||
gfx_w0_map(lvl_page);
|
||||
rbg = room_bg_ptr(room);
|
||||
for (i = 0; i < ROOM_TILES; i++) out30[i] = rbg[i];
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
uint8_t pop_room_link(uint8_t room, uint8_t side)
|
||||
{
|
||||
uint8_t v;
|
||||
if (!lvl_ok || room < 1 || room > 24 || side > 3) return 0;
|
||||
gfx_w0_map(lvl_page);
|
||||
v = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(uint16_t)(room - 1) * 4 + side];
|
||||
gfx_w0_unmap();
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint8_t start_byte(uint16_t off)
|
||||
{
|
||||
uint8_t v;
|
||||
if (!lvl_ok) return 0;
|
||||
gfx_w0_map(lvl_page);
|
||||
v = ((const uint8_t *)(LVL_DATA_OFF))[off];
|
||||
gfx_w0_unmap();
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
|
||||
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
|
||||
* колонки (pos_guards), поэтому guards_x из файла не читаем. */
|
||||
/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ *
|
||||
* В оригинале массивы стражей — часть структуры уровня в ОЗУ, и движок их
|
||||
* ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие
|
||||
* позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq. Благодаря
|
||||
* этому, вернувшись, находишь труп там же, а не воскресшего стража.
|
||||
* У нас уровень лежит в EMM-странице только на чтение, поэтому держим
|
||||
* мутабельную копию в W2: 6 байт на комнату. */
|
||||
#define GS_TILE 0
|
||||
#define GS_DIR 1
|
||||
#define GS_X 2
|
||||
#define GS_SKILL 3
|
||||
#define GS_SEQL 4
|
||||
#define GS_SEQH 5
|
||||
static uint8_t gstate[24 * 6];
|
||||
|
||||
static void gstate_init(void)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
|
||||
uint8_t r;
|
||||
gfx_w0_map(lvl_page);
|
||||
for (r = 0; r < 24; r++) {
|
||||
uint8_t *g = gstate + (uint16_t)r * 6;
|
||||
g[GS_TILE] = p[BP_GTILE + r];
|
||||
g[GS_DIR] = p[BP_GDIR + r];
|
||||
g[GS_X] = p[BP_GX + r];
|
||||
g[GS_SKILL] = p[BP_GSKILL + r];
|
||||
/* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
|
||||
* комнатах (оригинал чистит их в reset_level_unused_fields).
|
||||
* Осмысленное значение туда кладёт только leave_guard, поэтому
|
||||
* стартуем нулями: 0 = «поднимать стандартной стойкой».
|
||||
* Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
|
||||
g[GS_SEQL] = 0;
|
||||
g[GS_SEQH] = 0;
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты. seq
|
||||
* сохраняем ТОЛЬКО у мёртвого — у живого seq_hi = 0 означает «поднять
|
||||
* стандартной стойкой», как в оригинале. */
|
||||
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
|
||||
uint8_t skill, uint16_t seq, uint8_t dead)
|
||||
{
|
||||
uint8_t *g;
|
||||
if (room < 1 || room > 24) return;
|
||||
g = gstate + (uint16_t)(room - 1) * 6;
|
||||
g[GS_TILE] = tile;
|
||||
g[GS_DIR] = (uint8_t)dir;
|
||||
g[GS_X] = x;
|
||||
g[GS_SKILL] = skill;
|
||||
if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
|
||||
else { g[GS_SEQH] = 0; }
|
||||
}
|
||||
|
||||
uint16_t pop_guard_state_seq(uint8_t room)
|
||||
{
|
||||
const uint8_t *g;
|
||||
if (room < 1 || room > 24) return 0;
|
||||
g = gstate + (uint16_t)(room - 1) * 6;
|
||||
if (g[GS_SEQH] == 0) return 0; /* живой — стандартная стойка */
|
||||
return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
|
||||
}
|
||||
|
||||
/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
|
||||
* центре тайла). */
|
||||
uint8_t pop_guard_state_x(uint8_t room)
|
||||
{
|
||||
if (room < 1 || room > 24) return 0;
|
||||
return gstate[(uint16_t)(room - 1) * 6 + GS_X];
|
||||
}
|
||||
|
||||
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||
uint8_t *color, uint8_t *skill)
|
||||
{
|
||||
const uint8_t *g;
|
||||
uint8_t t;
|
||||
if (!lvl_ok || room < 1 || room > 24) return 0;
|
||||
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
|
||||
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
|
||||
g = gstate + (uint16_t)(room - 1) * 6;
|
||||
t = g[GS_TILE];
|
||||
if (t < 30) {
|
||||
*tile = t;
|
||||
*dir = (int8_t)g[GS_DIR];
|
||||
*skill = g[GS_SKILL];
|
||||
gfx_w0_map(lvl_page);
|
||||
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
return (uint8_t)(t < 30);
|
||||
}
|
||||
|
||||
uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); }
|
||||
uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); }
|
||||
int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); }
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* pop_level.h — данные уровня PoP из сырого файла res200N.bin (Фаза L1).
|
||||
*
|
||||
* Уровень загружается ОДИН раз в отдельную EMM-страницу (как атласы:
|
||||
* данные с offset 0x100, ISR-стаб в первых байтах — страница безопасна
|
||||
* для маппинга в W0). Файл — сырой blueprnt DAT 1.0 (2305 байт, БЕЗ
|
||||
* checksum-байта; Table 6 спецификации POP-DAT):
|
||||
* foretable(fg) @0 backtable(bg) @720 door I/II @1440/1696
|
||||
* links @1952 (24×{L,R,U,D}) start @2112 ...
|
||||
* Тайл-код = байт fg & 0x1F (верхние биты — модификатор, пока не нужны);
|
||||
* bg передаётся как есть. См. applications/PoP/docs/POP-DAT-* .
|
||||
*
|
||||
* Рабочая копия ТЕКУЩЕЙ комнаты живёт в обычной памяти (W2) у приложения
|
||||
* (мутабельная — loose→empty); pop_room_load извлекает её из страницы +
|
||||
* тонкие срезы соседей для кромок (правый столбец left-комнаты, верхний
|
||||
* ряд down-комнаты). Страница маппится в W0 только на время извлечения.
|
||||
*/
|
||||
#ifndef POP_LEVEL_H
|
||||
#define POP_LEVEL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
|
||||
* (open/alloc/размер). Звать ПОСЛЕ первого atlas_load (pop_bg_load) —
|
||||
* он снимает страницу ядра DSS для W0-unmap. */
|
||||
int pop_level_load(const char *path);
|
||||
void pop_level_free(void);
|
||||
|
||||
/* Извлечь комнату room (1..24) в массивы приложения (W2):
|
||||
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
|
||||
* lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
|
||||
* (кромка col0), [3..5] предпоследний (col8) —
|
||||
* для коллизии на колонке −2 (Kid стоит В шве);
|
||||
* rcol_fg[6] симметрично (col0 / col1);
|
||||
* нет комнаты → стена/0;
|
||||
* below_fg[10] — верхний ряд (row0) комнаты СНИЗУ («стена
|
||||
* вниз»); нет комнаты → стена.
|
||||
* 0 — OK, -1 — уровень не загружен / room вне диапазона. */
|
||||
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
|
||||
uint8_t *lcol_fg, uint8_t *lcol_bg,
|
||||
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg);
|
||||
|
||||
/* Связь комнаты по стороне (0=L,1=R,2=U,3=D) → номер соседней (1..24) или
|
||||
* 0, если стороны нет. Для переходов между комнатами (L2/L3). */
|
||||
uint8_t pop_room_link(uint8_t room, uint8_t side);
|
||||
|
||||
/* Тайл-код (fg & 0x1F) по комнате (1..24) и позиции tilepos (0..29,
|
||||
* row*10+col); вне диапазона → 0 (empty). Для trob-диспетчера (pop_trob):
|
||||
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
|
||||
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
|
||||
|
||||
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
|
||||
* Между begin/end — только pop_level_tile_raw, без рисования. */
|
||||
void pop_level_access_begin(void);
|
||||
void pop_level_access_end(void);
|
||||
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos);
|
||||
|
||||
/* Скопировать backtable (модификаторы) комнаты room в out[30]. Исходное
|
||||
* состояние per-room modifier (openness ворот, состояние пик и т.п.) для
|
||||
* ленивой инициализации room_modif в pop_trob. Вне диапазона → нули. */
|
||||
void pop_level_room_bg(uint8_t room, uint8_t *out30);
|
||||
|
||||
/* Таблицы связей дверей (LINKLOC@1440 / LINKMAP@1696, по 256 Б). Копируются
|
||||
* в W2-RAM при загрузке (LINKMAP мутабельна — таймеры кнопок). i — индекс
|
||||
* цепочки (модификатор кнопки). Декод — в pop_trob (get_doorlink_*). */
|
||||
uint8_t pop_doorlink1(uint8_t i);
|
||||
uint8_t pop_doorlink2(uint8_t i);
|
||||
void pop_doorlink2_set(uint8_t i, uint8_t v);
|
||||
|
||||
/* Ряд приземления падающего loose-куска в колонке col комнаты room: первый
|
||||
* НЕ-пустой тайл сверху вниз; возвращает его ряд, если это floor (→ debris),
|
||||
* иначе -1 (пролетел/крушение о стену). */
|
||||
int8_t pop_room_col_landing(uint8_t room, uint8_t col);
|
||||
|
||||
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
|
||||
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
|
||||
* Порт enter_guard (seg002:0112). */
|
||||
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
|
||||
* копии массивов уровня. dead != 0 — сохранить и последовательность, тогда
|
||||
* при возврате в комнату страж поднимется трупом там же, где лёг.
|
||||
* tile == 30 (или больше) — «стража в комнате больше нет». */
|
||||
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
|
||||
uint8_t skill, uint16_t seq, uint8_t dead);
|
||||
|
||||
/* Последовательность запомненного стража (0 — поднимать стандартной
|
||||
* стойкой, как у живого). */
|
||||
uint16_t pop_guard_state_seq(uint8_t room);
|
||||
uint8_t pop_guard_state_x(uint8_t room);
|
||||
|
||||
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||
uint8_t *color, uint8_t *skill);
|
||||
|
||||
uint8_t pop_level_start_room(void);
|
||||
uint8_t pop_level_start_pos(void);
|
||||
int8_t pop_level_start_dir(void);
|
||||
|
||||
#endif
|
||||
@@ -14,17 +14,113 @@
|
||||
#include <stdint.h>
|
||||
|
||||
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
|
||||
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). */
|
||||
void pop_map_set(const uint8_t *fg);
|
||||
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
||||
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
|
||||
* что и в pop_room_draw, чтобы отрисовка видела изменения. */
|
||||
void pop_map_set(uint8_t *fg) __banked;
|
||||
|
||||
/* Установлен в 1, когда Kid упал ниже комнаты (комнаты снизу нет в этом
|
||||
* тесте). Приложение должно сбросить Kid (kid_init) и снять флаг. */
|
||||
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
|
||||
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
|
||||
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
|
||||
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
|
||||
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
|
||||
const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked;
|
||||
|
||||
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
|
||||
* Звать в enter_room. */
|
||||
void pop_map_set_room(uint8_t room) __banked;
|
||||
|
||||
/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт
|
||||
* get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража
|
||||
* (guards.c): он идёт по ряду между Кидом и стражем. */
|
||||
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
|
||||
|
||||
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
|
||||
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
|
||||
* респавн). */
|
||||
extern uint8_t pop_kid_dead;
|
||||
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;
|
||||
|
||||
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
|
||||
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
|
||||
* tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл
|
||||
* (он стал полом) на ОБЕИХ страницах и записать override комнаты. */
|
||||
extern uint8_t pop_have_sword;
|
||||
extern uint8_t pop_item_taken;
|
||||
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
|
||||
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
|
||||
extern uint8_t pop_flash_time;
|
||||
/* Цвет вспышки: 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) */
|
||||
|
||||
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
|
||||
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
|
||||
* Приложение переключает комнату по соответствующей связи и снимает флаг. */
|
||||
extern uint8_t pop_leave_dir;
|
||||
|
||||
/* exit_room_timer (порт seg002 exit_room): пока >0, боковой/нижний leave
|
||||
* заблокирован (декремент в pop_phys_tick). Приложение ставит =2 при входе
|
||||
* в комнату (enter_room) — 2 кадра защиты от повторного/транзиентного leave
|
||||
* (ping-pong у шва, транзиент отскока о ворота, баг #4/BUG-2). */
|
||||
extern uint8_t pop_leave_timer;
|
||||
|
||||
/* Loose floors (проваливающиеся полы). pop_loose_modif[30] — состояние на
|
||||
* тайл (0 покой / 0x80.. тряска / 1..11 отсчёт падения); читает pop_bg при
|
||||
* отрисовке. pop_loose_tick() — анимация + перерисовка изменившихся тайлов
|
||||
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
|
||||
* рядом с kid-отрисовкой. */
|
||||
extern uint8_t pop_loose_modif[30];
|
||||
void pop_loose_tick(void) __banked;
|
||||
void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
|
||||
|
||||
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
|
||||
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
|
||||
extern uint8_t pop_loose_fell;
|
||||
|
||||
/* Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ (в оригинале — тот же loose с
|
||||
* отрицательным tilepos, seg006 check_press frame 79 / seg007 animate_loose).
|
||||
* pop_map_set_above даёт МУТАБЕЛЬНУЮ копию ряда 2 верхнего соседа (ту же, что
|
||||
* ушла в pop_room_set_above): провалившаяся плита становится в ней EMPTY —
|
||||
* сверху открывается колодец. pop_ceil_modif[col] — состояние (как
|
||||
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
|
||||
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
|
||||
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
|
||||
void pop_map_set_above(uint8_t *above_row2_fg) __banked;
|
||||
extern uint8_t pop_ceil_modif[10];
|
||||
extern uint8_t pop_ceil_fell;
|
||||
extern uint8_t pop_debris_at;
|
||||
|
||||
/* Взводится в кадре, когда Kid получил урон (hitp_delta<0 оригинала) —
|
||||
* приложение рисует «брызги» kid_draw_splash() поверх Kid и снимает флаг. */
|
||||
extern uint8_t pop_kid_hurt;
|
||||
|
||||
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
|
||||
* приземления/удара. Зовётся из pop_phys_tick. */
|
||||
void pop_check_knock(void) __banked;
|
||||
|
||||
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
|
||||
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
|
||||
* комнаты снизу (y -= 189, curr_row = 0). Приложение решает: если есть
|
||||
* links.down — сменить комнату (pop_room_load+перерисовка) и продолжить;
|
||||
* иначе — выпал из уровня (респавн). Снять флаг после обработки. */
|
||||
extern uint8_t pop_fell_out;
|
||||
|
||||
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
|
||||
* determine_col + check_action (триггер падения / do_fall / приземление).
|
||||
* Порядок как в оригинале (seg000 play_kid_frame). */
|
||||
void pop_phys_tick(void);
|
||||
void pop_phys_tick(void) __banked;
|
||||
|
||||
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
|
||||
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
|
||||
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
|
||||
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
|
||||
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
|
||||
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
|
||||
|
||||
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
|
||||
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
|
||||
@@ -33,26 +129,26 @@ void pop_phys_tick(void);
|
||||
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
|
||||
|
||||
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
|
||||
int pop_edge_distance(void);
|
||||
uint8_t pop_edge_type(void);
|
||||
int pop_edge_distance(void) __banked;
|
||||
uint8_t pop_edge_type(void) __banked;
|
||||
|
||||
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
|
||||
* позиции (передний край у стены, симметрично) и возвращает 1. Для
|
||||
* control_running (чистый стоп у стены). */
|
||||
int pop_wall_ahead(void);
|
||||
int pop_wall_ahead(void) __banked;
|
||||
|
||||
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
|
||||
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
|
||||
* check_jump_up/grab_up (K4.4). control_up гасит caller. */
|
||||
uint8_t pop_jump_up_seq(void);
|
||||
uint8_t pop_jump_up_seq(void) __banked;
|
||||
|
||||
/* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
|
||||
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
|
||||
* pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */
|
||||
uint8_t pop_hang_grab_ready(void); /* grab_timer истёк — можно climb-up */
|
||||
uint8_t pop_hang_climb_up_seq(void); /* seq_10 / seq_73 (подтягивание) */
|
||||
int8_t pop_hang_wall(void); /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
|
||||
uint8_t pop_hang_fall_seq(void); /* seq_23 / seq_11 (сорваться), +Kid.x */
|
||||
uint8_t pop_down_action(void); /* 1=обработано(спуск/отступ), 0=присед */
|
||||
uint8_t pop_hang_grab_ready(void) __banked; /* grab_timer истёк — можно climb-up */
|
||||
uint8_t pop_hang_climb_up_seq(void) __banked; /* seq_10 / seq_73 (подтягивание) */
|
||||
int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
|
||||
uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
|
||||
uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* pop_redraw.c — пометки перерисовки тайлов и их разбор. См. pop_redraw.h.
|
||||
*
|
||||
* Живёт в W1/W2 (НЕ в резиденте W3): пометки ставит логика, которая должна
|
||||
* оставаться достижимой из будущих __banked-модулей. Сам разбор зовёт
|
||||
* pop_bg (резидент W3) — это законно, потому что разбор дёргает только
|
||||
* главный цикл.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "pop_redraw.h"
|
||||
#include "pop_bg.h"
|
||||
|
||||
#define NTILES 30
|
||||
#define NCOLS 10
|
||||
|
||||
static uint8_t rd_kind[NTILES], rd_cnt[NTILES];
|
||||
static uint8_t rda_kind[NCOLS], rda_cnt[NCOLS];
|
||||
/* Сколько всего непустых пометок: без него проход по 40 ячейкам крутился бы
|
||||
* КАЖДЫЙ кадр впустую (замер 2026-07-27 на прежнем rest-цикле: ~12 % кадра). */
|
||||
static uint8_t rd_pending;
|
||||
|
||||
void pop_redraw_reset(void)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < NTILES; i++) { rd_kind[i] = 0; rd_cnt[i] = 0; }
|
||||
for (i = 0; i < NCOLS; i++) { rda_kind[i] = 0; rda_cnt[i] = 0; }
|
||||
rd_pending = 0;
|
||||
}
|
||||
|
||||
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
|
||||
{
|
||||
if (tilepos >= NTILES) return;
|
||||
if (!rd_cnt[tilepos]) rd_pending++;
|
||||
rd_kind[tilepos] = kind;
|
||||
if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages;
|
||||
}
|
||||
|
||||
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
|
||||
{
|
||||
if (col >= NCOLS) return;
|
||||
if (!rda_cnt[col]) rd_pending++;
|
||||
rda_kind[col] = kind;
|
||||
if (pages > rda_cnt[col]) rda_cnt[col] = pages;
|
||||
}
|
||||
|
||||
/* redraw_needed (seg008:0178): пройти пометки и нарисовать. row/col ведём
|
||||
* бегущими счётчиками, а не через i/10 и i%10 — у Z80 нет деления, и SDCC
|
||||
* выносит __divuchar/__moduchar в вершину цикла как общее подвыражение
|
||||
* аргументов (30*2 делений за кадр даже когда рисовать нечего). */
|
||||
void pop_redraw_needed(void)
|
||||
{
|
||||
uint8_t i, row = 0, col = 0, left = 0;
|
||||
if (!rd_pending) return;
|
||||
for (i = 0; i < NTILES; i++) {
|
||||
if (rd_cnt[i]) {
|
||||
switch (rd_kind[i]) {
|
||||
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
|
||||
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
|
||||
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break;
|
||||
case POP_RD_FLOOR: pop_floor_bake(row, col); break;
|
||||
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
|
||||
default: break;
|
||||
}
|
||||
if (--rd_cnt[i]) left++;
|
||||
else rd_kind[i] = 0;
|
||||
}
|
||||
if (++col == 10) { col = 0; row++; }
|
||||
}
|
||||
for (i = 0; i < NCOLS; i++) {
|
||||
if (rda_cnt[i]) {
|
||||
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
|
||||
else pop_ceil_bake_empty(i);
|
||||
if (--rda_cnt[i]) left++;
|
||||
else rda_kind[i] = 0;
|
||||
}
|
||||
}
|
||||
rd_pending = left;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* pop_redraw.h — пометки «перерисовать тайл» и их разбор.
|
||||
*
|
||||
* Порт архитектуры оригинала: логика анимации НИЧЕГО не рисует, она ставит
|
||||
* флаг (`set_redraw_full`/`set_wipe`/`redraw_20h`/`redraw_21h`, seg007), а
|
||||
* отрисовка идёт отдельным проходом (`redraw_needed`, seg008:0178). У нас
|
||||
* так же: pop_map/pop_trob зовут pop_set_redraw*, а pop_redraw_needed()
|
||||
* (главный цикл) разбирает пометки и дёргает pop_bg.
|
||||
*
|
||||
* ЗАЧЕМ (кроме сходства с SDLPoP): из __banked-кода резидентная страница W3
|
||||
* недостижима — ни напрямую, ни транзитивно (docs/layout_plan_v2.md §2 R2).
|
||||
* Пока pop_map звал pop_bg, его нельзя было использовать из банка стражей.
|
||||
* Пометки живут в W1/W2 и зовутся откуда угодно.
|
||||
*
|
||||
* ОТЛИЧИЕ ОТ ОРИГИНАЛА: у нас не один экран с dirty-rect, а две страницы
|
||||
* дабл-буфера со своей ОЗУ-копией фона, поэтому счётчик пометки — это ЧИСЛО
|
||||
* СТРАНИЦ (обычно 2), а вид перерисовки хранится явно: heal+рисовать поверх
|
||||
* (транзиент) или запечь новый фон. В оригинале вид кодируется тем, в какой
|
||||
* из таблиц redraw_frames_* стоит флаг.
|
||||
*/
|
||||
#ifndef POP_REDRAW_H
|
||||
#define POP_REDRAW_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Виды перерисовки тайла (row*10+col). */
|
||||
#define POP_RD_SPIKE 1 /* пики: heal + оба тайла (redraw_21h) */
|
||||
#define POP_RD_LOOSE 2 /* loose: дрожащий кадр / кадр покоя */
|
||||
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
|
||||
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
|
||||
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
|
||||
|
||||
/* Виды перерисовки полосы у потолка (по колонке). */
|
||||
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
|
||||
#define POP_RDA_CEIL_GONE 2 /* плита-потолок провалилась: колодец */
|
||||
|
||||
/* Пометить тайл/колонку: kind — вид, pages — на скольких страницах
|
||||
* дабл-буфера перерисовать (1 = только текущая, 2 = обе). Повторная
|
||||
* пометка перекрывает вид и берёт максимум счётчика. */
|
||||
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages);
|
||||
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages);
|
||||
|
||||
/* Разобрать пометки: нарисовать всё помеченное на ТЕКУЩЕЙ (back) странице и
|
||||
* уменьшить счётчики. Звать из главного цикла в слое ФОНА — до kid_draw. */
|
||||
void pop_redraw_needed(void);
|
||||
|
||||
/* Сбросить все пометки (смена комнаты/уровня). */
|
||||
void pop_redraw_reset(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* pop_state.c — данные, которые читают ОБЕ стороны банковой границы.
|
||||
*
|
||||
* Зачем отдельный модуль: sprinter-cc собирает банковый модуль с
|
||||
* --dataseg BANKn, поэтому ВСЯ его писучая память (глобалы, file-static,
|
||||
* статики внутри функций) линкуется В СТРАНИЦУ БАНКА и снаружи не читается
|
||||
* — проверено на .map (2026-07-30: глобал банкового модуля лёг по
|
||||
* 0x0001C000, а не в _DATA). Значит любую переменную, к которой нужен
|
||||
* доступ и из банка, и снаружи, надо держать в НЕбанковом модуле — здесь.
|
||||
*
|
||||
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
|
||||
*/
|
||||
#include "pop_state.h"
|
||||
|
||||
uint8_t pop_loose_landed;
|
||||
|
||||
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
|
||||
* но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */
|
||||
uint8_t pop_immortal;
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* pop_state.h — состояние, общее для банков и W1/W2. См. pop_state.c.
|
||||
*
|
||||
* Правило: если переменную читают/пишут по обе стороны банковой границы —
|
||||
* её место здесь, а НЕ в банковом модуле (там она уедет в страницу банка).
|
||||
*/
|
||||
#ifndef POP_STATE_H
|
||||
#define POP_STATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
|
||||
* tilepos+1 тайла, на который он лёг. Ставит отрисовка mob (банк),
|
||||
* разбирает логика loose-полов (pop_map, W1/W2). */
|
||||
extern uint8_t pop_loose_landed;
|
||||
|
||||
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
|
||||
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
|
||||
extern uint8_t pop_immortal;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
* pop_trob.c — P0-каркас: room_modif + список trob + диспетчер анимаций.
|
||||
* Порт SDLPoP seg007 (process_trobs/animate_tile/add_trob/animate_spike).
|
||||
* См. pop_trob.h.
|
||||
*/
|
||||
#include "pop_trob.h"
|
||||
#include "pop_level.h"
|
||||
#include "pop_geom.h"
|
||||
#include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */
|
||||
/* pop_bg нужен ТОЛЬКО для пламени факела и пузырька зелья: это не тайловая
|
||||
* перерисовка, а покадровый оверлей поверх фона (в оригинале — свои
|
||||
* add_backtable/add_foretable внутри draw_tile). Всё остальное идёт через
|
||||
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
|
||||
* нельзя звать из __banked-кода. */
|
||||
#include "pop_bg.h"
|
||||
|
||||
/* Коды тайлов (fg & 0x1F), которые анимируются. */
|
||||
#define TILE_SPIKE 0x02
|
||||
#define TILE_GATE 0x04
|
||||
#define TILE_CLOSER 0x06
|
||||
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
|
||||
#define TILE_OPENER 0x0F
|
||||
#define TILE_POTION 0x0A
|
||||
#define TILE_SWORD 0x16 /* меч-предмет на полу */
|
||||
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
|
||||
#define TILE_TORCH 0x13
|
||||
#define TILE_TORCH_DEBRIS 0x1E
|
||||
|
||||
/* ---- Персистентное per-room modifier-состояние -------------------- */
|
||||
static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES];
|
||||
static uint8_t room_seen[POP_NROOMS]; /* modif проинициализирован из уровня */
|
||||
|
||||
/* ---- Список анимируемых тайлов ------------------------------------ */
|
||||
typedef struct { uint8_t room, tilepos; int8_t type; } trob_t;
|
||||
#define TROBS_MAX 30 /* как в оригинале (types.h) */
|
||||
static trob_t trobs[TROBS_MAX];
|
||||
static uint8_t trobs_count;
|
||||
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
|
||||
|
||||
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
|
||||
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
|
||||
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
|
||||
* стоящим Кидом перерисовывалась КАЖДЫЙ кадр (2×draw_tile ≈ 50% кадра в
|
||||
* статике), т.к. её trob висит, пока Кид стоит (это верно, как в SDLPoP —
|
||||
* расхождение было именно в безусловном редрое). */
|
||||
static uint8_t trob_drawn[POP_ROOMTILES];
|
||||
|
||||
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
|
||||
* выбора следующего кадра пламени. */
|
||||
static pop_rnd_t trob_seed;
|
||||
|
||||
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
|
||||
|
||||
|
||||
void pop_trob_reset(void)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
|
||||
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||
pop_redraw_reset();
|
||||
trobs_count = 0;
|
||||
}
|
||||
|
||||
uint8_t *pop_trob_modif(uint8_t room)
|
||||
{
|
||||
if (room < 1 || room > POP_NROOMS) return 0;
|
||||
if (!room_seen[room - 1]) {
|
||||
uint8_t i;
|
||||
pop_level_room_bg(room, room_modif[room - 1]);
|
||||
/* seg009 load_level: у ЗЕЛЬЯ модификатор сдвигается влево на 3 —
|
||||
* старшие биты = тип зелья, младшие 3 = фаза пузырька. */
|
||||
pop_level_access_begin();
|
||||
for (i = 0; i < POP_ROOMTILES; i++)
|
||||
if ((pop_level_tile_raw(room, i) & 0x1F) == TILE_POTION)
|
||||
room_modif[room - 1][i] = (uint8_t)(room_modif[room - 1][i] << 3);
|
||||
pop_level_access_end();
|
||||
room_seen[room - 1] = 1;
|
||||
}
|
||||
return room_modif[room - 1];
|
||||
}
|
||||
|
||||
static int find_trob(uint8_t room, uint8_t tilepos)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < trobs_count; i++)
|
||||
if (trobs[i].room == room && trobs[i].tilepos == tilepos) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type)
|
||||
{
|
||||
int f = find_trob(room, tilepos);
|
||||
if (f >= 0) { trobs[f].type = type; return; }
|
||||
if (trobs_count >= TROBS_MAX) return;
|
||||
trobs[trobs_count].room = room;
|
||||
trobs[trobs_count].tilepos = tilepos;
|
||||
trobs[trobs_count].type = type;
|
||||
trobs_count++;
|
||||
}
|
||||
|
||||
void pop_trob_room_changed(void)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||
pop_redraw_reset();
|
||||
}
|
||||
|
||||
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
|
||||
* тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch,
|
||||
* seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька
|
||||
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
|
||||
void pop_trob_anim_room(uint8_t room)
|
||||
{
|
||||
uint8_t *mod = pop_trob_modif(room);
|
||||
uint8_t i, code;
|
||||
if (!mod) return;
|
||||
pop_level_access_begin();
|
||||
/* Освободить список от «декоративных» trob ДРУГИХ комнат: факел, зелье и
|
||||
* блеск меча анимируются только в ОТРИСОВАННОЙ комнате, а место в списке
|
||||
* занимают. Оригинал их копит и на 30-й выдаёт «Trobs Overflow»; у нас
|
||||
* отладочный обход всех 24 комнат упирается в лимит за пару минут, после
|
||||
* чего новые анимации молча не заводятся. Механику (ворота/пики/кнопки,
|
||||
* в т.ч. кросс-комнатные) чистка не трогает. */
|
||||
{
|
||||
uint8_t k, wr = 0;
|
||||
for (k = 0; k < trobs_count; k++) {
|
||||
uint8_t c = (uint8_t)(pop_level_tile_raw(trobs[k].room,
|
||||
trobs[k].tilepos) & 0x1F);
|
||||
if (trobs[k].room != room &&
|
||||
(c == TILE_TORCH || c == TILE_TORCH_DEBRIS ||
|
||||
c == TILE_POTION || c == TILE_SWORD))
|
||||
continue;
|
||||
trobs[wr++] = trobs[k];
|
||||
}
|
||||
trobs_count = wr;
|
||||
}
|
||||
for (i = 0; i < POP_ROOMTILES; i++) {
|
||||
code = (uint8_t)(pop_level_tile_raw(room, i) & 0x1F);
|
||||
if (code == TILE_TORCH || code == TILE_TORCH_DEBRIS) {
|
||||
mod[i] = (uint8_t)trob_prandom(8);
|
||||
pop_add_trob(room, i, 1);
|
||||
} else if (code == TILE_POTION) {
|
||||
mod[i] = (uint8_t)((mod[i] & 0xF8) | (trob_prandom(6) + 1));
|
||||
pop_add_trob(room, i, 1);
|
||||
} else if (code == TILE_SWORD) {
|
||||
/* start_anim_sword (seg007:087C): случайная фаза счётчика блеска. */
|
||||
mod[i] = (uint8_t)(trob_prandom(0xFF) & 0x1F);
|
||||
pop_add_trob(room, i, 1);
|
||||
}
|
||||
}
|
||||
pop_level_access_end();
|
||||
}
|
||||
|
||||
/* animate_spike (seg007:04D3): автомат по modifier. *m — modifier тайла,
|
||||
* *type — тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4),
|
||||
* на 5 → 0x8F (полностью вышли, безвредны), на 9 → 0 + конец. Убирание
|
||||
* (флаг 0x80): --modif; когда 0x7F-часть обнулилась → modif=6 (начать
|
||||
* визуальный уход 6..8). 0xFF = disabled. */
|
||||
/* get_torch_frame (seg007:06CD): случайный кадр 0..8, иначе следующий. */
|
||||
static uint8_t get_torch_frame(uint8_t curr)
|
||||
{
|
||||
uint8_t next = (uint8_t)trob_prandom(255);
|
||||
if (next != curr && next < 9) return next;
|
||||
next = (uint8_t)(curr + 1);
|
||||
if (next >= 9) next = 0;
|
||||
return next;
|
||||
}
|
||||
|
||||
/* bubble_next_frame (seg007:0759): фазы пузырька 1..7 по кругу. */
|
||||
static uint8_t bubble_next_frame(uint8_t curr)
|
||||
{
|
||||
uint8_t next = (uint8_t)(curr + 1);
|
||||
return (next >= 8) ? 1 : next;
|
||||
}
|
||||
|
||||
static void animate_spike(uint8_t *m, int8_t *type)
|
||||
{
|
||||
uint8_t mod = *m;
|
||||
if (*type >= 0) {
|
||||
if (mod == 0xFF) return; /* disabled — не двигать */
|
||||
if (mod & 0x80) {
|
||||
--mod;
|
||||
if (mod & 0x7F) { *m = mod; return; }
|
||||
mod = 6;
|
||||
} else {
|
||||
++mod;
|
||||
if (mod == 5) mod = 0x8F;
|
||||
else if (mod == 9) { mod = 0; *type = -1; }
|
||||
}
|
||||
}
|
||||
*m = mod;
|
||||
}
|
||||
|
||||
/* ---- Кнопки/ворота (seg007) --------------------------------------- *
|
||||
* Таблицы связей LINKLOC/LINKMAP — в pop_level (мутабельная копия в W2).
|
||||
* doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */
|
||||
static uint8_t get_doorlink_timer(uint8_t i) { return (uint8_t)(pop_doorlink2(i) & 0x1F); }
|
||||
static void set_doorlink_timer(uint8_t i, uint8_t v)
|
||||
{
|
||||
pop_doorlink2_set(i, (uint8_t)((pop_doorlink2(i) & 0xE0) | (v & 0x1F)));
|
||||
}
|
||||
static uint8_t get_doorlink_tile(uint8_t i) { return (uint8_t)(pop_doorlink1(i) & 0x1F); }
|
||||
static uint8_t get_doorlink_next(uint8_t i) { return (uint8_t)(!(pop_doorlink1(i) & 0x80)); }
|
||||
static uint8_t get_doorlink_room(uint8_t i)
|
||||
{
|
||||
return (uint8_t)(((pop_doorlink1(i) & 0x60) >> 5) + ((pop_doorlink2(i) & 0xE0) >> 3));
|
||||
}
|
||||
|
||||
/* trigger_gate (seg007:092C): изменить openness ворот по типу кнопки.
|
||||
* Возвращает тип анимации для add_trob (или -1 = ничего). */
|
||||
static int8_t trigger_gate(uint8_t room, uint8_t tilepos, uint8_t button_type)
|
||||
{
|
||||
uint8_t *mod = pop_trob_modif(room);
|
||||
uint8_t modifier = mod[tilepos];
|
||||
if (button_type == TILE_OPENER) {
|
||||
if (modifier == 0xFF) return -1; /* уже насовсем открыт */
|
||||
if (modifier >= 188) { mod[tilepos] = 238; return -1; } /* открыт → держать */
|
||||
mod[tilepos] = (uint8_t)((modifier + 3) & 0xFC);
|
||||
return 1; /* обычное открытие */
|
||||
} else if (button_type == TILE_DEBRIS) { /* насовсем (died_on_button) */
|
||||
if (modifier < 188) return 2;
|
||||
mod[tilepos] = 0xFF;
|
||||
return -1;
|
||||
} else { /* closer */
|
||||
if (modifier != 0) return 3; /* быстро закрыть */
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* trigger_1 (seg007:0999): цель = ворота → trigger_gate; иначе -1 (leveldoor
|
||||
* и прочее — отложено). */
|
||||
static int8_t trigger_1(uint8_t target_type, uint8_t room, uint8_t tilepos, uint8_t button_type)
|
||||
{
|
||||
if (target_type == TILE_GATE)
|
||||
return trigger_gate(room, tilepos, button_type);
|
||||
if (target_type == TILE_LEVELDOOR) {
|
||||
/* trigger_1 (seg007:0999): дверь уровня открывается ОДИН раз — если
|
||||
* створка уже тронулась (modif != 0), кнопка ничего не делает. */
|
||||
uint8_t *m = pop_trob_modif(room);
|
||||
if (m && m[tilepos] == 0) return 1;
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* do_trigger_list (seg007:09E5): обойти цепочку LINKLOC от index, для каждой
|
||||
* цели добавить trob (если trigger_1 >= 0). */
|
||||
static void do_trigger_list(uint8_t index, uint8_t button_type)
|
||||
{
|
||||
for (;;) {
|
||||
uint8_t room = get_doorlink_room(index);
|
||||
uint8_t tilepos = get_doorlink_tile(index);
|
||||
uint8_t target_type = pop_level_tile(room, tilepos);
|
||||
int8_t result = trigger_1(target_type, room, tilepos, button_type);
|
||||
if (result >= 0) pop_add_trob(room, tilepos, result);
|
||||
if (get_doorlink_next(index) == 0) break;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
||||
uint8_t button_type, uint8_t modifier)
|
||||
{
|
||||
uint8_t link_timer = get_doorlink_timer(modifier);
|
||||
if (link_timer != 0x1F) { /* не заклинено */
|
||||
set_doorlink_timer(modifier, 5);
|
||||
if (link_timer < 2)
|
||||
pop_add_trob(room, tilepos, 1); /* кнопка нажимается (+ TODO sound) */
|
||||
do_trigger_list(modifier, button_type);
|
||||
}
|
||||
}
|
||||
|
||||
/* animate_button (seg007:0D3A): отсчитать таймер связи; <2 → кнопка
|
||||
* отжимается (trob кончился). idx = модификатор кнопки (индекс LINKLOC). */
|
||||
static void animate_button(uint8_t idx, int8_t *type)
|
||||
{
|
||||
if (*type >= 0) {
|
||||
uint8_t timer = get_doorlink_timer(idx);
|
||||
timer = (uint8_t)(timer ? timer - 1 : 0);
|
||||
set_doorlink_timer(idx, timer);
|
||||
if (timer < 2) *type = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* animate_leveldoor (seg007:05F1): створка двери уровня. type 0..2 —
|
||||
* открытие (modif 0 -> 43, по пикселю за кадр), type >= 3 — быстрое
|
||||
* закрытие со скоростями {0,5,17,99} (после выхода из уровня). Готово ->
|
||||
* type = -1. */
|
||||
static const uint8_t leveldoor_close_speeds[5] = {0, 5, 17, 99, 0};
|
||||
|
||||
static void animate_leveldoor(uint8_t *m, int8_t *type)
|
||||
{
|
||||
if (*type < 0) return;
|
||||
if (*type >= 3) {
|
||||
uint8_t sp;
|
||||
if (*type < 7) (*type)++;
|
||||
sp = leveldoor_close_speeds[*type - 3];
|
||||
if (sp >= *m) { *m = 0; *type = -1; }
|
||||
else *m = (uint8_t)(*m - sp);
|
||||
} else {
|
||||
if (*m < 43) (*m)++;
|
||||
if (*m >= 43) { *m = 43; *type = -1; }
|
||||
}
|
||||
}
|
||||
|
||||
/* animate_door (seg007:0522): openness по типу анимации. *m — modif ворот,
|
||||
* *type — 0 закрытие / 1 открытие / 2 насовсем / 3..8 быстрое закрытие. */
|
||||
static const uint8_t gate_close_speeds[9] = {0, 0, 0, 20, 40, 60, 80, 100, 120};
|
||||
static const int8_t door_delta[3] = {-1, 4, 4};
|
||||
|
||||
static void animate_door(uint8_t *m, int8_t *type)
|
||||
{
|
||||
int8_t anim_type = *type;
|
||||
uint8_t mod = *m;
|
||||
if (anim_type >= 0) {
|
||||
if (anim_type >= 3) { /* быстрое закрытие */
|
||||
if (anim_type < 8) { anim_type++; *type = anim_type; }
|
||||
{
|
||||
int new_mod = (int)mod - gate_close_speeds[anim_type];
|
||||
if (new_mod < 0) { mod = 0; *type = -1; }
|
||||
else mod = (uint8_t)new_mod;
|
||||
}
|
||||
} else if (mod != 0xFF) {
|
||||
mod = (uint8_t)(mod + door_delta[anim_type]);
|
||||
if (anim_type == 0) { /* закрытие */
|
||||
if (mod == 0) *type = -1; /* gate_stop */
|
||||
} else { /* открытие */
|
||||
if (mod >= 188) {
|
||||
if (anim_type < 2) { mod = 238; *type = 0; } /* открыт → потом закрывать */
|
||||
else { mod = 0xFF; *type = -1; } /* насовсем */
|
||||
}
|
||||
}
|
||||
} else {
|
||||
*type = -1;
|
||||
}
|
||||
}
|
||||
*m = mod;
|
||||
}
|
||||
|
||||
uint8_t pop_trob_room_active(uint8_t room)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < trobs_count; i++)
|
||||
if (trobs[i].room == room) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 3
|
||||
* vsync и соответствует игровому тику оригинала):
|
||||
* 1 — кадр пламени каждый тик, как в оригинале; кадры при этом ровные по
|
||||
* тактам, что нужно для счёта бюджета;
|
||||
* 2 — вдвое реже (так было до 2026-07-30; пламя шло на половинной
|
||||
* скорости, а бюджет кадра «прыгал» на 6.2 % от того, попал кадр на
|
||||
* анимацию или нет). */
|
||||
#define TORCH_ANIM_DIV 1
|
||||
|
||||
#if TORCH_ANIM_DIV > 1
|
||||
static uint8_t torch_tick;
|
||||
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
|
||||
#else
|
||||
#define TORCH_ANIM_NOW 1
|
||||
#endif
|
||||
|
||||
void pop_process_trobs(uint8_t cur_room)
|
||||
{
|
||||
uint8_t i, w = 0;
|
||||
#if TORCH_ANIM_DIV > 1
|
||||
torch_tick++;
|
||||
#endif
|
||||
|
||||
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
|
||||
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
|
||||
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
|
||||
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
|
||||
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
|
||||
if (trobs_count == 0) return;
|
||||
|
||||
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
|
||||
* pop_level_tile() на каждый trob (тот свапал W0 каждый вызов = N свапов
|
||||
* за кадр даже без анимации). Дальше цикл (с редроями) идёт с
|
||||
* размапленным W0 — как раньше. */
|
||||
{
|
||||
uint8_t k;
|
||||
pop_level_access_begin();
|
||||
for (k = 0; k < trobs_count; k++)
|
||||
trob_code[k] = pop_level_tile_raw(trobs[k].room, trobs[k].tilepos);
|
||||
pop_level_access_end();
|
||||
}
|
||||
|
||||
for (i = 0; i < trobs_count; i++) {
|
||||
uint8_t room = trobs[i].room;
|
||||
uint8_t tp = trobs[i].tilepos;
|
||||
uint8_t code = trob_code[i];
|
||||
uint8_t *mod = pop_trob_modif(room);
|
||||
int8_t type = trobs[i].type;
|
||||
|
||||
switch (code) {
|
||||
case TILE_SPIKE:
|
||||
animate_spike(&mod[tp], &type);
|
||||
break;
|
||||
case TILE_OPENER:
|
||||
case TILE_CLOSER:
|
||||
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
|
||||
break;
|
||||
case TILE_GATE:
|
||||
animate_door(&mod[tp], &type);
|
||||
break;
|
||||
case TILE_LEVELDOOR:
|
||||
animate_leveldoor(&mod[tp], &type);
|
||||
break;
|
||||
case TILE_TORCH:
|
||||
case TILE_TORCH_DEBRIS:
|
||||
/* animate_torch (seg007:03C1): анимация вечная — type остаётся
|
||||
* 1, пока комната отрисована. Темп — TORCH_ANIM_DIV. */
|
||||
if (room == cur_room && TORCH_ANIM_NOW)
|
||||
mod[tp] = get_torch_frame(mod[tp]);
|
||||
break;
|
||||
case TILE_POTION:
|
||||
/* animate_potion (seg007:03E9): крутим младшие 3 бита (фаза
|
||||
* пузырька), старшие 5 — тип зелья. */
|
||||
if (room == cur_room)
|
||||
mod[tp] = (uint8_t)((mod[tp] & 0xF8) | bubble_next_frame(mod[tp] & 7));
|
||||
break;
|
||||
case TILE_SWORD:
|
||||
/* animate_sword (seg007:0425): счётчик вниз; на 0 — новый период
|
||||
* 0x28..0x67. Блеск рисуется РОВНО на modif==1 (draw_tile берёт
|
||||
* кадр (modif==1)+10), т.е. одиночная вспышка раз в 40..103 тика. */
|
||||
if (room == cur_room) {
|
||||
if (--mod[tp] == 0)
|
||||
mod[tp] = (uint8_t)((trob_prandom(255) & 0x3F) + 0x28);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
type = -1;
|
||||
break;
|
||||
}
|
||||
trobs[i].type = type;
|
||||
|
||||
if (room == cur_room &&
|
||||
(code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) {
|
||||
/* Пламя/пузырёк анимируются КАЖДЫЙ кадр и в фон не запечены —
|
||||
* рисуем поверх (сами делают heal своей области). */
|
||||
uint8_t trow = tp / 10, tcol = tp % 10;
|
||||
if (code == TILE_POTION) pop_potion_draw(trow, tcol, mod[tp]);
|
||||
else pop_torch_draw(trow, tcol, mod[tp]);
|
||||
}
|
||||
if (room == cur_room && code == TILE_SPIKE) {
|
||||
/* ПИКИ — редрой КАЖДЫЙ кадр, пока trob жив: animate_spike
|
||||
* (seg007:0353) зовёт redraw_21h БЕЗУСЛОВНО, а не по смене кадра.
|
||||
* Это не «лишняя работа оригинала»: выдвинутая пика живёт только в
|
||||
* видео-ОЗУ (банк SPRITE, в фон не запечена), а kid_heal каждый кадр
|
||||
* возвращает под спрайтом Кида ПЕЧЁНЫЙ фон — то есть стирает те
|
||||
* части остриёв, что попали под его прямоугольник. При редрое
|
||||
* по смене кадра пика держится 15 кадров (modif 0x8F..0x81) с
|
||||
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с
|
||||
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
|
||||
* странице дабл-буфера из двух). */
|
||||
/* Последний кадр (пика убралась, trob умирает) — пометить ОБЕ
|
||||
* страницы: живой trob метит каждый кадр по одной. */
|
||||
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_LEVELDOOR) {
|
||||
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
|
||||
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
|
||||
* kid_heal вернул бы из фона закрытую створку. Куски двери
|
||||
* непрозрачны, поэтому wipe не нужен. */
|
||||
/* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
|
||||
* помечаем на обе страницы, иначе дверь дрожит через кадр. */
|
||||
if (tp % 10 + 1 < 10)
|
||||
pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR,
|
||||
(uint8_t)(type < 0 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_SWORD) {
|
||||
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
|
||||
* спрайт запечён в фон (draw_tile), поэтому перерисовываем тайл
|
||||
* той же схемой, что и кнопку — текущая страница сразу, вторая
|
||||
* следующим кадром через rest-цикл. */
|
||||
uint8_t sig = (uint8_t)(mod[tp] == 1);
|
||||
if (sig != trob_drawn[tp]) {
|
||||
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
|
||||
trob_drawn[tp] = sig;
|
||||
}
|
||||
}
|
||||
if (room == cur_room && (code == TILE_OPENER || code == TILE_CLOSER)) {
|
||||
/* КНОПКА — редрой только при смене видимого состояния (нажата =
|
||||
* таймер связи >1): пока Кид её держит, картинка стабильна.
|
||||
* Рисуем ТЕКУЩУЮ страницу сразу + <rest>=1 (вторая страница
|
||||
* дабл-буфера следующим кадром через rest-цикл). tp/10,tp%10
|
||||
* считаем только в момент редроя (редко), а не каждый кадр. */
|
||||
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
|
||||
if (sig != trob_drawn[tp]) {
|
||||
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
|
||||
trob_drawn[tp] = sig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* compact: удалить завершённые (type < 0). */
|
||||
for (i = 0; i < trobs_count; i++)
|
||||
if (trobs[i].type >= 0) trobs[w++] = trobs[i];
|
||||
trobs_count = w;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* pop_trob.h — P0-каркас: персистентное per-room modifier-состояние +
|
||||
* список анимируемых тайлов (trob). Порт SDLPoP seg007 (process_trobs/
|
||||
* add_trob/animate_tile) + curr_room_modif.
|
||||
*
|
||||
* ЗАЧЕМ: тайлы-объекты (пики, ворота, кнопки) имеют состояние (modifier),
|
||||
* которое живёт МЕЖДУ входами в комнату и может меняться КРОСС-КОМНАТНО
|
||||
* (кнопка в одной комнате открывает ворота в другой). room_modif[room]
|
||||
* — единственный источник модификаторов для отрисовки (pop_bg draw_tile
|
||||
* читает его как bg), как curr_room_modif в оригинале.
|
||||
*
|
||||
* Список trob хранит (room, tilepos, type) активных анимаций; каждый кадр
|
||||
* pop_process_trobs продвигает их (animate_spike/door/button по коду тайла),
|
||||
* пишет результат в room_modif и перерисовывает изменившиеся тайлы ТЕКУЩЕЙ
|
||||
* комнаты на back-странице (2-page, как loose). Невидимые комнаты
|
||||
* анимируются в состоянии (без отрисовки) — при входе рисуются статически.
|
||||
*/
|
||||
#ifndef POP_TROB_H
|
||||
#define POP_TROB_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_NROOMS 24
|
||||
#define POP_ROOMTILES 30
|
||||
|
||||
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
|
||||
* очистить список trob. */
|
||||
void pop_trob_reset(void);
|
||||
|
||||
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
|
||||
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
|
||||
* (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне
|
||||
* диапазона → 0. */
|
||||
uint8_t *pop_trob_modif(uint8_t room);
|
||||
|
||||
/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить
|
||||
* type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */
|
||||
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type);
|
||||
|
||||
/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать
|
||||
* modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице.
|
||||
* Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */
|
||||
void pop_process_trobs(uint8_t cur_room);
|
||||
|
||||
/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку
|
||||
* LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button.
|
||||
* room/tilepos — кнопка; button_type — её тайл-код; modifier — её modif
|
||||
* (= индекс в LINKLOC). Звать из check_press (pop_map). */
|
||||
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
||||
uint8_t button_type, uint8_t modifier);
|
||||
|
||||
/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через
|
||||
* шов (roomtest не дёргает шов, пока сосед не анимируется). */
|
||||
uint8_t pop_trob_room_active(uint8_t room);
|
||||
|
||||
/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты
|
||||
* (trob-состояние в room_modif персистентно и НЕ трогается). Звать в
|
||||
* enter_room. */
|
||||
void pop_trob_room_changed(void);
|
||||
|
||||
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
|
||||
* зелья) со случайной стартовой фазой. Звать в enter_room. */
|
||||
void pop_trob_anim_room(uint8_t room);
|
||||
|
||||
#endif
|
||||
@@ -1,10 +0,0 @@
|
||||
/* room1_data.h — данные комнаты 1 ур.1 (res2001) + левый столбец
|
||||
* соседа (room 5 col9) для границы-решётки. Сгенерировано
|
||||
* gen из render_room; fg=foretable code, bg=backtable modifier. */
|
||||
#ifndef ROOM1_DATA_H
|
||||
#define ROOM1_DATA_H
|
||||
static const unsigned char room1_fg[30] = {0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x14,0x14,0x13,0x13,0x01,0x03,0x00,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x0E,0x03,0x0B,0x01,0x01,0x14};
|
||||
static const unsigned char room1_bg[30] = {0xFF,0xFF,0xFF,0x00,0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00};
|
||||
static const unsigned char leftcol_fg[3] = {0x04,0x14,0x14};
|
||||
static const unsigned char leftcol_bg[3] = {0x01,0x00,0x00};
|
||||
#endif
|
||||
@@ -13,30 +13,265 @@
|
||||
#include "pop_kid.h"
|
||||
#include "pop_ctrl.h"
|
||||
#include "pop_map.h"
|
||||
#include "room1_data.h"
|
||||
#include "pop_level.h"
|
||||
#include "pop_trob.h"
|
||||
#include "pop_redraw.h"
|
||||
#include "pop_guard.h"
|
||||
#include "pop_cheat.h"
|
||||
|
||||
__sfr __at (0xFE) io_border;
|
||||
|
||||
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
|
||||
* выключить). Цвет бордюра держится, пока фаза считает, поэтому высота
|
||||
* полосы на скриншоте = время фазы в растровых строках. Это единственный
|
||||
* способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
|
||||
* отвечает из фреймового колбэка, все сэмплы падают в обработчик
|
||||
* прерывания); точные такты — breakpoint'ы с {printf totalcycles; g}.
|
||||
*
|
||||
* ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
|
||||
* бит 1 (синий) и бит 2 (зелёный) — значит различимых состояний ровно
|
||||
* ЧЕТЫРЕ, и значения обязаны быть чётными. Отсюда фазы сгруппированы по
|
||||
* четыре, а не по семь (нечётные номера раньше сливались с соседями и
|
||||
* полосы не читались):
|
||||
* 0 чёрный — ожидание vsync (запас кадра)
|
||||
* 2 синий — ввод, heal и ЛОГИКА (kid_tick/физика/страж)
|
||||
* 4 зелёный — слой ФОНА (loose/trobs/redraw_needed/шов)
|
||||
* 6 циан — СПРАЙТЫ (страж, Kid, fore поверх них) */
|
||||
#ifndef PROF_BORDER
|
||||
#define PROF_BORDER 1
|
||||
#endif
|
||||
#if PROF_BORDER
|
||||
#define PROF(n) io_border = (n)
|
||||
#else
|
||||
#define PROF(n) ((void)0)
|
||||
#endif
|
||||
|
||||
/* Сколько банков грузит crt0_banked (читает его стартовый код).
|
||||
* 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
|
||||
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP).
|
||||
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
|
||||
* вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних
|
||||
* уровней, чомперы, зеркало. */
|
||||
const uint8_t n_banks = 4;
|
||||
|
||||
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
||||
uint8_t pop_cheats;
|
||||
|
||||
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
|
||||
#define SEQ_STAND 2
|
||||
#define ROOM_1 1
|
||||
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
||||
|
||||
/* Тест-старт для отладки пик (Фаза S): комната 6 = тестовая (пики в col3/col4
|
||||
* 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_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
|
||||
|
||||
/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
|
||||
* ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (1..24, с обёрткой),
|
||||
* Kid ставится на первый попавшийся пол. Нужен, чтобы пройти все комнаты и
|
||||
* собрать список багов отрисовки, включая недостижимые обычным путём.
|
||||
* Убирается закомментированием ОДНОЙ строки ниже. */
|
||||
#define ROOMNAV 1
|
||||
|
||||
|
||||
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
|
||||
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
|
||||
* чтобы enter_room переключал комнату без длинного списка параметров. */
|
||||
static uint8_t room_fg[30], room_bg[30];
|
||||
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
|
||||
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
|
||||
* смотрит наружу (колонка −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 above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
|
||||
static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
|
||||
static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
|
||||
|
||||
/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid
|
||||
* слева от отрисованной → −140 (спрайт за левой кромкой), справа → +140, та
|
||||
* же → 0. Звать каждый кадр перед kid_draw. */
|
||||
static void update_kid_render_dx(void)
|
||||
{
|
||||
int dx = 0;
|
||||
if (kid_room != cur_room) {
|
||||
if (kid_room == pop_room_link(cur_room, 0)) dx = -140; /* left neighbour */
|
||||
else if (kid_room == pop_room_link(cur_room, 1)) dx = 140; /* right neighbour */
|
||||
}
|
||||
pop_kid_set_render_dx(dx);
|
||||
}
|
||||
|
||||
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
|
||||
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
|
||||
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
|
||||
* → 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен
|
||||
* — тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера. */
|
||||
static uint8_t seam_sig; /* последняя нарисованная openness гейта в шве */
|
||||
static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */
|
||||
|
||||
/* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая):
|
||||
* sig = последняя показанная комната, redraw = остаток кадров (2 = обе страницы). */
|
||||
static uint8_t room_label_sig = 0xFF;
|
||||
static uint8_t room_label_redraw;
|
||||
|
||||
/* Персистентные переопределения тайлов уровня: упавший loose-кусок оставил
|
||||
* щебень (floor → debris), а на месте самой плиты — пусто (в т.ч. в комнате
|
||||
* СВЕРХУ: провалившийся потолок = колодец). Минимальный per-room override:
|
||||
* живёт между переходами, применяется в enter_room поверх загруженных тайлов
|
||||
* (и для ряда 2 верхнего соседа). (Полноценное per-room modifier-состояние —
|
||||
* в docs/gates_spikes_plan.md, P0.) */
|
||||
#define TILE_DEBRIS 0x0E
|
||||
#define TILE_EMPTY 0x00
|
||||
#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 и счётчик «запечь пол» по страницам.
|
||||
* flash_on — вспышка фона активна (нулевая запись палитры подменена). */
|
||||
static uint8_t item_pos, item_bake, flash_on;
|
||||
|
||||
/* Подмена НУЛЕВОЙ записи палитры на обеих страницах (do_flash/set_bg_attr).
|
||||
* Через функцию с параметрами, а не пятью литералами в месте вызова: на
|
||||
* gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
|
||||
* (см. .sprinter-cc-roomtest/roomtest.asm) и ассемблер падает. */
|
||||
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
gfx_pal_set(0, 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 в рабочие массивы и отрисовать её фон в ОБЕ
|
||||
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
|
||||
* активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
|
||||
static void enter_room(uint8_t room)
|
||||
{
|
||||
uint8_t pg;
|
||||
pop_guard_leave(); /* запомнить стража уходящей комнаты */
|
||||
uint8_t *room_mod;
|
||||
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_edges(pop_room_link(room, 0), pop_room_link(room, 1),
|
||||
pop_room_link(room, 2), pop_room_link(room, 3),
|
||||
lcol_fg, rcol_fg);
|
||||
pop_room_set_below(below_fg);
|
||||
/* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика
|
||||
* уровня, mod — живой room_modif (ленивая инициализация). */
|
||||
{
|
||||
uint8_t up = pop_room_link(room, 2), c;
|
||||
if (up) {
|
||||
uint8_t *um = pop_trob_modif(up);
|
||||
pop_level_access_begin();
|
||||
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
|
||||
pop_level_access_end();
|
||||
apply_overrides(up, above_fg, 20, 10); /* провалившийся потолок = колодец */
|
||||
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
|
||||
pop_room_set_above(above_fg, above_mod);
|
||||
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
|
||||
} else {
|
||||
pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */
|
||||
pop_map_set_above(0);
|
||||
}
|
||||
}
|
||||
pop_map_set_room(room);
|
||||
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
|
||||
* (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
|
||||
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
|
||||
* стража никогда не срабатывает. */
|
||||
Kid.room = room;
|
||||
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
|
||||
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
|
||||
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
|
||||
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
|
||||
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
|
||||
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
||||
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
||||
* а не по свежему backtable. Фаза P0. */
|
||||
room_mod = pop_trob_modif(room);
|
||||
/* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот,
|
||||
* видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6).
|
||||
* pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */
|
||||
{
|
||||
uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1);
|
||||
if (lft) { uint8_t *m = pop_trob_modif(lft);
|
||||
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
|
||||
seam_sig = m[9]; } /* шов рисуется в обе страницы ниже — синхр. сигнатуру */
|
||||
if (rgt) { uint8_t *m = pop_trob_modif(rgt);
|
||||
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; }
|
||||
}
|
||||
seam_redraw = 0; /* шов свежий на обеих страницах — редрой не нужен */
|
||||
for (pg = 0; pg < 2; pg++) {
|
||||
gfx_set_draw_page(pg);
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
bar(0, 0, 319, 255);
|
||||
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
|
||||
}
|
||||
cur_room = room;
|
||||
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
|
||||
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
|
||||
* по умолчанию (без мерцания); SPACE переключает в однобуфер для
|
||||
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
|
||||
uint8_t pg, front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
|
||||
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
|
||||
uint16_t dead_frames = 0;
|
||||
#ifdef ROOMNAV
|
||||
uint8_t nav_prev = 0;
|
||||
#endif
|
||||
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0;
|
||||
|
||||
if (pop_bg_load() != 0) { /* атласы — ДО initgraph */
|
||||
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
|
||||
puts("pop_bg_load failed");
|
||||
return 1;
|
||||
}
|
||||
pop_cheats = 1; /* режим разработки: читы включены */
|
||||
pop_guard_reset();
|
||||
if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
|
||||
printf("kid_data.bin load failed\n");
|
||||
return 1;
|
||||
}
|
||||
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
|
||||
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
||||
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
||||
puts("kid atlas load failed");
|
||||
return 1;
|
||||
}
|
||||
if (pop_level_load("LEVELS\\res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */
|
||||
pop_level_load("a:\\res2001.bin") != 0) {
|
||||
puts("level load failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
initgraph();
|
||||
gfx_set_visible_page(0);
|
||||
if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */
|
||||
if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */
|
||||
gfx_pal_fload(0, "a:\\kid.pal");
|
||||
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
|
||||
|
||||
@@ -44,18 +279,17 @@ int main(void)
|
||||
* ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
|
||||
* страницы, в которую рисуем. Дальше каждый тик восстанавливаем
|
||||
* ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
|
||||
for (pg = 0; pg < 2; pg++) {
|
||||
gfx_set_draw_page(pg);
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
bar(0, 0, 319, 255);
|
||||
pop_room_draw(1, room1_fg, room1_bg, leftcol_fg, leftcol_bg);
|
||||
}
|
||||
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
|
||||
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
|
||||
pop_trob_reset();
|
||||
enter_room(START_ROOM);
|
||||
gfx_set_draw_page(0);
|
||||
|
||||
/* K2b: Kid стоит на уступе (row1), управляется с клавиатуры.
|
||||
* ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. */
|
||||
pop_map_set(room1_fg); /* карта для коллизии/падения (K3) */
|
||||
kid_init(SEQ_STAND, 1, 1, 0);
|
||||
* ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход.
|
||||
* Карта коллизии (pop_map_set) уже задана в enter_room. */
|
||||
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
|
||||
pop_kid_hp_reset();
|
||||
kid_draw();
|
||||
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
||||
kid_fp_width(), kid_fp_height(),
|
||||
@@ -70,6 +304,9 @@ int main(void)
|
||||
while (!kbd_raw_down(KBD_ESC)) {
|
||||
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||
* переключ). Выключение возвращает показ на страницу 0. */
|
||||
|
||||
PROF(2); /* ввод + heal */
|
||||
|
||||
uint8_t sp = kbd_raw_down(KBD_SPACE);
|
||||
if (sp && !sp_prev) {
|
||||
dbuf ^= 1;
|
||||
@@ -85,32 +322,242 @@ int main(void)
|
||||
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
|
||||
if (kbd_raw_down(0x16)) frozen = 1;
|
||||
if (kbd_raw_down(0x1E)) frozen = 0;
|
||||
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
|
||||
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
|
||||
* K — убить стража;
|
||||
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
|
||||
* иначе боёвку проверять получается ровно три раза);
|
||||
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
|
||||
* берётся с пола (pop_have_sword ставит подбор). */
|
||||
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);
|
||||
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;
|
||||
cheat_prev = k;
|
||||
immo_prev = i;
|
||||
sword_prev = s;
|
||||
}
|
||||
if (frozen) {
|
||||
PROF(0); /* чёрный: ждём vsync */
|
||||
gfx_wait_vsync();
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef ROOMNAV
|
||||
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
|
||||
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
|
||||
* комната. pop_trob_reset ДО enter_room — каждая комната смотрится в
|
||||
* исходном состоянии (пики убраны, ворота закрыты). */
|
||||
{
|
||||
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
|
||||
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
|
||||
if (nav && !nav_prev) {
|
||||
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
|
||||
r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
|
||||
: (uint8_t)(r == 1 ? 24 : r - 1);
|
||||
pop_trob_reset();
|
||||
enter_room(r);
|
||||
/* Предпочитаем обычный пол; иначе — любой tile_is_floor
|
||||
* (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
|
||||
for (i = 0; i < 30; i++) {
|
||||
c = (uint8_t)(room_fg[i] & 0x1F);
|
||||
if (c == 1) { any = i; break; }
|
||||
if (any == 0xFF && c && c != 9 && c != 12 && c != 20 &&
|
||||
!(c >= 26 && c <= 29)) any = i;
|
||||
}
|
||||
if (any != 0xFF) pos = any;
|
||||
kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0);
|
||||
pop_kid_hp_reset();
|
||||
dead_frames = 0;
|
||||
}
|
||||
nav_prev = nav;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0. */
|
||||
back = dbuf ? (uint8_t)(front ^ 1) : 0;
|
||||
gfx_set_draw_page(back);
|
||||
|
||||
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
|
||||
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
|
||||
pop_guard_heal(); /* и прошлый кадр стража */
|
||||
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
|
||||
PROF(2); /* логика (тот же синий) */
|
||||
kid_tick(); /* play_seq: следующий кадр */
|
||||
pop_phys_tick(); /* K3: падение/приземление/стена */
|
||||
if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */
|
||||
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 пересёк нижнюю границу комнаты */
|
||||
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
||||
pop_fell_out = 0;
|
||||
kid_init(SEQ_STAND, 1, 1, 0);
|
||||
if (down) { /* L2: переход в комнату снизу — продолжает падать */
|
||||
enter_room(down); /* координаты Kid уже репроецированы pop_map */
|
||||
gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */
|
||||
} else { /* выпал из уровня (нет комнаты снизу) — респавн */
|
||||
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */
|
||||
enter_room(START_ROOM);
|
||||
gfx_set_draw_page(back);
|
||||
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
|
||||
pop_kid_hp_reset();
|
||||
}
|
||||
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
|
||||
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
|
||||
pop_leave_dir = 0;
|
||||
if (nb) {
|
||||
enter_room(nb);
|
||||
gfx_set_draw_page(back);
|
||||
}
|
||||
}
|
||||
kid_draw(); /* нарисовать новый кадр */
|
||||
|
||||
PROF(4); /* слой фона */
|
||||
|
||||
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
|
||||
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
|
||||
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
|
||||
/* process_trobs — рисования внутри нет) */
|
||||
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
|
||||
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
|
||||
{ /* ворота соседа слева (room8) видны в col0 — */
|
||||
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
|
||||
if (lft) {
|
||||
uint8_t *m = pop_trob_modif(lft);
|
||||
uint8_t g = m[9], prev = seam_sig;
|
||||
seam_sig = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */
|
||||
/* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */
|
||||
/* не g — sdcc_z80_cmp_store_a_bug) */
|
||||
if (g != prev) seam_redraw = 2; /* openness изменился → обе страницы дабл-буфера */
|
||||
if (seam_redraw) {
|
||||
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
|
||||
pop_room_redraw_seam_left();
|
||||
seam_redraw--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
|
||||
dead_frames++;
|
||||
if (dead_frames > RESPAWN_SETTLE &&
|
||||
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
|
||||
dead_frames = 0;
|
||||
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */
|
||||
enter_room(START_ROOM);
|
||||
gfx_set_draw_page(back);
|
||||
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
|
||||
pop_kid_hp_reset();
|
||||
}
|
||||
}
|
||||
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
|
||||
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
|
||||
uint8_t fcol = (uint8_t)(fpos % 10);
|
||||
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
||||
pop_loose_fell = 0;
|
||||
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
|
||||
if (down) {
|
||||
int8_t lrow = pop_room_col_landing(down, fcol);
|
||||
if (lrow >= 0) add_tile_override(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
|
||||
}
|
||||
}
|
||||
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
|
||||
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
|
||||
uint8_t up = pop_room_link(cur_room, 2);
|
||||
pop_ceil_fell = 0;
|
||||
if (up) add_tile_override(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
|
||||
}
|
||||
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
|
||||
add_tile_override(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
|
||||
pop_debris_at = 0;
|
||||
}
|
||||
if (pop_item_taken) { /* предмет поднят → тайл стал полом */
|
||||
item_pos = (uint8_t)(pop_item_taken - 1);
|
||||
item_bake = 2; /* запечь на ОБЕИХ страницах */
|
||||
add_tile_override(cur_room, item_pos, TILE_FLOOR);
|
||||
pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
|
||||
pop_item_taken = 0;
|
||||
}
|
||||
if (item_bake) { /* стереть спрайт меча из фона страницы */
|
||||
pop_floor_bake(item_pos / 10, item_pos % 10);
|
||||
item_bake--;
|
||||
}
|
||||
/* Вспышка фона — порт flash_if_hurt/remove_flash_if_hurt (seg003:0AFC):
|
||||
* do_flash(color) = set_bg_attr(0, color), т.е. подмена НУЛЕВОЙ записи
|
||||
* палитры (вспыхивает всё чёрное поле). Оригинал ставит цвет и СНИМАЕТ
|
||||
* его В ТОМ ЖЕ кадре, поэтому пока идёт flash_time видно СТРОБ
|
||||
* «жёлтый/чёрный», а не одну длинную заливку. Снятие — после первого
|
||||
* gfx_wait_vsync (ниже): цвет держится ~1 растровый кадр из трёх.
|
||||
* Приоритет как в оригинале: активный flash_time > кадр урона
|
||||
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
|
||||
if (pop_flash_time) {
|
||||
/* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (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--;
|
||||
if (!pop_flash_time) pop_flash_red = 0;
|
||||
flash_on = 1;
|
||||
} else if (pop_kid_hurt) {
|
||||
flash_bg(255, 85, 85); /* color_12_brightred */
|
||||
flash_on = 1;
|
||||
}
|
||||
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
|
||||
PROF(6); /* спрайты: страж, Kid, fore поверх */
|
||||
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */
|
||||
PROF(6); /* kid_draw (тот же циан) */
|
||||
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
|
||||
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
|
||||
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
|
||||
kid_draw_splash();
|
||||
pop_kid_hurt = 0;
|
||||
}
|
||||
pop_hp_draw(); /* полоса HP — только при изменении */
|
||||
PROF(6); /* fore поверх Kid + борта */
|
||||
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
||||
kid_fp_width(), kid_fp_height(),
|
||||
Kid.direction, Kid.frame, Kid.action);
|
||||
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
|
||||
|
||||
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже
|
||||
* сделан (скрытая страница), wait_vsync ловит начало кадра,
|
||||
* set_visible_page переключает во время бланка (gfx.h паттерн). */
|
||||
|
||||
PROF(0); /* чёрный: ждём vsync */
|
||||
|
||||
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
|
||||
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
|
||||
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
|
||||
#ifdef ROOMNAV
|
||||
{
|
||||
uint8_t r = cur_room, prev = room_label_sig;
|
||||
room_label_sig = r; /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */
|
||||
if (r != prev) room_label_redraw = 2;
|
||||
if (room_label_redraw) {
|
||||
/* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный
|
||||
* знакогенератор (_gfx_font_buf, 2 КБ статики в W2) и сажает
|
||||
* кучу с 3867 до 576 Б. Слева десятки, справа единицы:
|
||||
* || |||| = 24. bar/setfillstyle уже используются. */
|
||||
uint8_t i;
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
bar(100, 0, 175, 5);
|
||||
/* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная
|
||||
* (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */
|
||||
setfillstyle(SOLID_FILL, 0x57);
|
||||
for (i = 0; i < (uint8_t)(r / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5);
|
||||
for (i = 0; i < (uint8_t)(r % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5);
|
||||
room_label_redraw--;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
gfx_wait_vsync();
|
||||
if (flash_on) { /* строб: цвет держался один кадр растра */
|
||||
flash_bg(0, 0, 0);
|
||||
flash_on = 0;
|
||||
}
|
||||
gfx_wait_vsync();
|
||||
gfx_wait_vsync();
|
||||
if (dbuf) {
|
||||
@@ -123,5 +570,8 @@ int main(void)
|
||||
closegraph();
|
||||
pop_bg_free();
|
||||
pop_kid_free();
|
||||
pop_guard_free();
|
||||
pop_kid_data_free();
|
||||
pop_level_free();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_
|
||||
import os
|
||||
import re
|
||||
|
||||
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src"
|
||||
SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "SDLPoP", "src")
|
||||
OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "poc", "res", "kid")
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "roomtest", "kid_data.h")
|
||||
SEQTBL_BASE = 0x196E
|
||||
@@ -24,10 +26,11 @@ def read(name):
|
||||
return open(os.path.join(SRC, name)).read()
|
||||
|
||||
|
||||
def parse_frame_table(txt):
|
||||
"""frame_table_kid[] -> список (image, dx, dy, flags). Записи вида
|
||||
{ IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как OR-выражения."""
|
||||
m = re.search(r'const frame_type frame_table_kid\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
|
||||
def parse_frame_table(txt, name="frame_table_kid"):
|
||||
"""frame_table_kid[]/frame_tbl_guard[] -> список (image, dx, dy, flags).
|
||||
Записи вида { IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как
|
||||
OR-выражения."""
|
||||
m = re.search(r'const frame_type ' + name + r'\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
|
||||
body = m.group(1)
|
||||
frames = []
|
||||
for row in re.findall(r'\{([^}]*)\}', body):
|
||||
@@ -41,6 +44,18 @@ def parse_frame_table(txt):
|
||||
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):
|
||||
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))]
|
||||
@@ -63,8 +78,10 @@ def main():
|
||||
seqc = read("seqtbl.c")
|
||||
|
||||
frames = parse_frame_table(seg006)
|
||||
gframes = parse_frame_table(seg006, "frame_tbl_guard")
|
||||
seqtbl = parse_byte_array(seqc, "original_seqtbl")
|
||||
offs = parse_word_array(seqc, "original_seqtbl_offsets")
|
||||
swtbl = parse_sword_tbl(seg006)
|
||||
|
||||
with open(OUT, "w") as f:
|
||||
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
|
||||
@@ -74,28 +91,57 @@ def main():
|
||||
f.write("#ifndef KID_DATA_H\n#define KID_DATA_H\n#include <stdint.h>\n\n")
|
||||
f.write("#define SEQTBL_BASE 0x%04Xu\n\n" % SEQTBL_BASE)
|
||||
|
||||
# frame table
|
||||
f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */\n")
|
||||
# frame table и seqtbl В .h НЕ ПОПАДАЮТ — они уезжают в kid_data.bin
|
||||
# (EMM-страница): 3.5 КБ в _CODE окна W1/W2 нам дороже, чем один
|
||||
# gfx_w0_map за тик. См. applications/PoP/docs/layout_plan_v2.md §5.1.
|
||||
f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid).\n"
|
||||
" * САМА таблица кадров и seqtbl лежат в kid_data.bin (EMM-\n"
|
||||
" * страница); здесь только тип и размеры. */\n")
|
||||
f.write("typedef struct { uint8_t image; int8_t dx, dy; "
|
||||
"uint8_t flags, sword; } kframe;\n")
|
||||
f.write("#define KID_NFRAMES %d\n" % len(frames))
|
||||
f.write("static const kframe kid_frames[KID_NFRAMES] = {\n")
|
||||
for (img, dx, dy, fl, sw) in frames:
|
||||
dxs = dx - 256 if dx > 127 else dx
|
||||
dys = dy - 256 if dy > 127 else dy
|
||||
f.write(" {%3d,%4d,%4d,0x%02X,0x%02X},\n" % (img, dxs, dys, fl, sw))
|
||||
f.write("};\n\n")
|
||||
f.write("#define KID_SEQTBL_LEN %d\n\n" % len(seqtbl))
|
||||
|
||||
# seqtbl bytes
|
||||
f.write("#define KID_SEQTBL_LEN %d\n" % len(seqtbl))
|
||||
f.write("static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {\n")
|
||||
f.write(emit_c(seqtbl) + "\n};\n\n")
|
||||
# раскладка kid_data.bin (смещения ОТ НАЧАЛА ДАННЫХ страницы)
|
||||
f.write("/* Раскладка kid_data.bin: кадры (KID_NFRAMES*5), затем seqtbl. */\n")
|
||||
f.write("#define KID_BIN_FRAMES_OFF 0\n")
|
||||
f.write("#define KID_BIN_SEQTBL_OFF %d\n" % (len(frames) * 5))
|
||||
f.write("/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс\n"
|
||||
" * frame + add_frame - 149 (add_frame=70 для кадров 102..106). */\n")
|
||||
f.write("#define KID_NGFRAMES %d\n" % len(gframes))
|
||||
f.write("#define KID_BIN_GFRAMES_OFF %d\n\n" % (len(frames) * 5 + len(seqtbl)))
|
||||
|
||||
# seq offsets
|
||||
f.write("#define KID_NSEQ %d\n" % len(offs))
|
||||
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("#endif\n")
|
||||
|
||||
# sword_tbl — макро-инициализаторами, а не готовыми массивами: так
|
||||
# 159 байт таблицы лягут в ОДИН TU (pop_kid.c), а не в каждый, кто
|
||||
# включил заголовок.
|
||||
f.write("/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.\n"
|
||||
" * Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.\n"
|
||||
" * Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),\n"
|
||||
" * x — «вперёд» по направлению персонажа. */\n")
|
||||
f.write("#define KID_NSWORD %d\n" % len(swtbl))
|
||||
for i, (nm, fmt) in enumerate((("ID", "0x%02X"), ("DX", "%d"), ("DY", "%d"))):
|
||||
# dx/dy — знаковые: печатаем со знаком, иначе int8_t = overflow
|
||||
vals = [r[i] if i == 0 else (r[i] - 256 if r[i] > 127 else r[i])
|
||||
for r in swtbl]
|
||||
f.write("#define KID_SWORD_%s_INIT { \\\n" % nm)
|
||||
f.write(emit_c(vals, 12, fmt).replace("\n", " \\\n") + " \\\n}\n")
|
||||
f.write("\n#endif\n")
|
||||
|
||||
# kid_data.bin: кадры (по 5 байт) + seqtbl подряд
|
||||
os.makedirs(OUT_BIN_DIR, exist_ok=True)
|
||||
binpath = os.path.join(OUT_BIN_DIR, "kid_data.bin")
|
||||
with open(binpath, "wb") as b:
|
||||
for (img, dx, dy, fl, sw) in frames:
|
||||
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
|
||||
b.write(bytes(seqtbl))
|
||||
for (img, dx, dy, fl, sw) in gframes:
|
||||
b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF)))
|
||||
print(f"kid_data.bin: {len(frames)*5 + len(seqtbl) + len(gframes)*5} Б (кадры Kid + seqtbl + кадры стража) -> {binpath}")
|
||||
|
||||
print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, "
|
||||
f"{len(offs)} seq-оффсетов -> {OUT}")
|
||||
|
||||
@@ -50,6 +50,66 @@ FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100}
|
||||
# попадает в used render_room, добавляем явно в env-фон. (32 обычно уже есть.)
|
||||
CLIMB_OVERLAY_ENV_IDS = {32, 150, 151}
|
||||
|
||||
# Дрожащие/падающие кадры loose-плиты (loose_fram_left/right/bottom, seg008):
|
||||
# 41/42/43 = покой (уже в used из render_room), 69-74 = вибрация/раздвоение —
|
||||
# используются ТОЛЬКО динамикой (shake/fall), в статический render_room НЕ
|
||||
# попадают, поэтому добавляем явно. Без них дрожащая плита невидима.
|
||||
LOOSE_ANIM_ENV_IDS = {69, 70, 71, 72, 73, 74}
|
||||
|
||||
# Кадры ВЫДВИЖЕНИЯ пик (seg008): три части — left (128-132, своя ячейка,
|
||||
# draw_tile_anim), right (134-138, правый сосед, draw_tile_anim_right), fore
|
||||
# (139-143, поверх Kid, draw_tile_fore). Рисуются ТОЛЬКО динамикой
|
||||
# (animate_spike); в уровнях пики статически убраны (modif=0 → кадр 0),
|
||||
# поэтому в used render_room НЕ попадают. Без них выдвинутые пики невидимы.
|
||||
SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left
|
||||
134, 135, 136, 137, 138, # right
|
||||
139, 140, 141, 142, 143} # fore
|
||||
|
||||
# Кадры ОТКРЫТИЯ ворот (portcullis, DOOR_FRAM_SLICE + gate pieces 50/51,
|
||||
# seg008 draw_gate): 50-59 + 67. В уровнях ворота статически ЗАКРЫТЫ
|
||||
# (низкий modif → используются только нижние срезы 50-53); срезы более
|
||||
# открытых состояний (54-59, 67) нужны анимации открытия — добавляем явно.
|
||||
GATE_ANIM_ENV_IDS = {50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 67}
|
||||
|
||||
# НАЖАТАЯ кнопка-closer (get_tile_to_draw seg008:2FE: при таймере связи >1
|
||||
# tiles_6_closer рисуется как tiles_5_stuck). tile_table[5] = база 41 (уже
|
||||
# есть), правая грань 35, нижняя 36 — оба id встречаются ТОЛЬКО в этом
|
||||
# динамическом состоянии, статически тайла 5 в уровнях нет, значит в used
|
||||
# render_room они не попадают. Без них у нажатой кнопки чёрный провал на
|
||||
# месте нижней грани и потерянная правая грань.
|
||||
STUCK_ENV_IDS = {35, 36}
|
||||
|
||||
# Левая половина кнопки-opener, когда СЛЕВА пусто (draw_tile_base seg008:0A8E:
|
||||
# curr_tile==15 && tile_left==0 && level_type==0 → id 148 вместо базы 147).
|
||||
# render_room этой ветки не знает, поэтому добавляем явно.
|
||||
OPENER_NOFLOOR_ENV_IDS = {148}
|
||||
|
||||
# ---- chtab_1 (flame/sword/potion), база ресурса 150, папка PRINCE ----
|
||||
# Зелье (seg008 draw_tile_fore/draw_tile_anim): склянка id 12 (малая) / 13
|
||||
# (большая, типы 2..4), кадры пузырька 16..22 (potion_fram_bubb) и его маска
|
||||
# 23. Пузырёк и маска рисуются MONO-блитом (одним цветом), поэтому пакуем их
|
||||
# силуэтами уже в нужном цвете: маска — чёрный env[0], пузырёк — env[12]
|
||||
# (красный, тип «лечение»). Цвета других типов (9 синий / 10 зелёный) —
|
||||
# TODO, когда появятся такие зелья.
|
||||
POT_DIR_NAME = "PRINCE"
|
||||
POT_FLAME_IDS = tuple(range(1, 10)) # пламя факела: modifier+1 (seg008:560)
|
||||
POT_FLASK_IDS = (12, 13)
|
||||
POT_BUBBLE_IDS = (16, 17, 18, 19, 20, 21, 22)
|
||||
POT_MASK_ID = 23
|
||||
POT_PAL_BASE = 0x40 # 16 цветов chtab_1 -> слоты 0x40..0x4F
|
||||
# MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из ОБЩЕЙ
|
||||
# 16-цветной палитры экрана, а не из палитры chtab. Кладём её в 0x30..0x3F.
|
||||
VGA16_PAL_BASE = 0x30
|
||||
VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(42,21,0),
|
||||
(42,42,42),(21,21,21),(21,21,63),(21,63,21),(21,63,63),(63,21,21),
|
||||
(63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal
|
||||
POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета)
|
||||
POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение»)
|
||||
|
||||
# Основание факела (seg008:489 draw_tile_right: при левом соседе-факеле
|
||||
# рисуется chtab_6 id 146). В статический render_room не попадает.
|
||||
TORCH_ENV_IDS = {146}
|
||||
|
||||
ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31
|
||||
ENV_MASK = (1 << ENV_SHIFT) - 1
|
||||
|
||||
@@ -96,9 +156,30 @@ def collect_used():
|
||||
|
||||
|
||||
# ---- 2. Загрузка спрайта в 8bpp-блоб ---------------------------------
|
||||
def load_indexed(chtab, id):
|
||||
# Спрайты chtab_6, которые оригинал рисует БЕЗ прозрачности
|
||||
# (blitters_0_no_transp): нижние грани тайлов (draw_tile_bottom, seg008:570) и
|
||||
# нижние кадры loose (draw_loose, seg008:600). У 96/149 (грани кнопок) и 73/74
|
||||
# (кадры loose) ПОСЛЕДНЯЯ строка — пиксель 0; в оригинале она заливается ЧЁРНЫМ
|
||||
# поверх того, что сзади, а у нас 0 -> 0xFF делал её прозрачной, и сквозь неё
|
||||
# просвечивал Kid (спуск/подъём у кромки кнопки). 43/36 полностью непрозрачны —
|
||||
# для них флаг ничего не меняет, оставлены для полноты списка.
|
||||
# Дверь уровня (draw_leveldoor, seg008:1D29): решётка створки (33 — слайс 4px,
|
||||
# 34 — верх), лестница за ней (99 — низ, 144 — марш). render_room.py
|
||||
# draw_leveldoor не реализует, поэтому в автосбор эти id не попадают.
|
||||
LEVELDOOR_ENV_IDS = {33, 34, 99, 144}
|
||||
|
||||
# Куски двери уровня (draw_leveldoor) оригинал рисует blitters_0_no_transp:
|
||||
# марш лестницы 144 обязан ЗАКРАШИВАТЬ поднявшуюся створку, иначе слева от
|
||||
# лестницы сквозь него видна решётка (0 -> 0xFF делал фон спрайта прозрачным).
|
||||
NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149} | LEVELDOOR_ENV_IDS
|
||||
|
||||
|
||||
def load_indexed(chtab, id, opaque=False):
|
||||
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
|
||||
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60)."""
|
||||
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).
|
||||
|
||||
opaque=True — пиксель 0 остаётся ЦВЕТОМ (base+0 = чёрный), а не ключом
|
||||
прозрачности: порт blitters_0_no_transp (см. NO_TRANSP_ENV_IDS)."""
|
||||
base_res = 360 if chtab == R.CHTAB_WALL else 200
|
||||
pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50
|
||||
fname = f"res{base_res + id}.png"
|
||||
@@ -114,7 +195,32 @@ def load_indexed(chtab, id):
|
||||
if im.mode != "P":
|
||||
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
|
||||
src = im.tobytes()
|
||||
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
|
||||
if opaque:
|
||||
out = bytes((pal_base + v) for v in src)
|
||||
else:
|
||||
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
|
||||
return im.width, im.height, out
|
||||
|
||||
|
||||
def load_potion(id, mono=None, opaque=False):
|
||||
"""chtab_1: res(150+id).png из data/PRINCE. mono=None — обычный спрайт
|
||||
(палитра chtab_1 -> слоты POT_PAL_BASE+i); mono=цвет — силуэт этим цветом
|
||||
(порт blitters_40h_mono); opaque=True — пиксель 0 остаётся ЦВЕТОМ
|
||||
(порт blitters_0_no_transp, см. POT_FLAME_IDS)."""
|
||||
path = os.path.join(os.path.dirname(R.VDUNGEON_DIR), POT_DIR_NAME,
|
||||
f"res{150 + id}.png")
|
||||
if not os.path.exists(path):
|
||||
die(f"нет файла {path}")
|
||||
im = Image.open(path)
|
||||
if im.mode != "P":
|
||||
die(f"res{150+id}: mode {im.mode}, ожидался P")
|
||||
src = im.tobytes()
|
||||
if mono is not None:
|
||||
out = bytes((0xFF if v == 0 else mono) for v in src)
|
||||
elif opaque:
|
||||
out = bytes((POT_PAL_BASE + v) for v in src)
|
||||
else:
|
||||
out = bytes((0xFF if v == 0 else POT_PAL_BASE + v) for v in src)
|
||||
return im.width, im.height, out
|
||||
|
||||
|
||||
@@ -185,6 +291,8 @@ def build_palette(out_path):
|
||||
|
||||
env = read_pal("res200.pal", R.VDUNGEON_DIR)
|
||||
wall = read_pal("res360.pal", R.VDUNGEON_DIR)
|
||||
pot = read_pal("res150.pal", os.path.join(os.path.dirname(R.VDUNGEON_DIR),
|
||||
POT_DIR_NAME))
|
||||
pal = bytearray(256 * 4)
|
||||
for i, (r, g, b) in enumerate(env):
|
||||
o = (0x50 + i) * 4
|
||||
@@ -192,6 +300,12 @@ def build_palette(out_path):
|
||||
for i, (r, g, b) in enumerate(wall):
|
||||
o = (0x60 + i) * 4
|
||||
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
for i, (r, g, b) in enumerate(pot):
|
||||
o = (POT_PAL_BASE + i) * 4
|
||||
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
for i, (r, g, b) in enumerate(VGA16):
|
||||
o = (VGA16_PAL_BASE + i) * 4
|
||||
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(pal)
|
||||
|
||||
@@ -228,6 +342,8 @@ def write_header(out_path, env_pages, wall_name, fore_name):
|
||||
"};",
|
||||
'#define POP_WALL_ATL "%s"' % wall_name,
|
||||
'#define POP_FORE_ATL "%s"' % fore_name,
|
||||
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */',
|
||||
"#define POP_PAL_POT 0x%02X" % POT_PAL_BASE,
|
||||
'#define POP_BG_PAL "pop_bg.pal"',
|
||||
"",
|
||||
"#endif",
|
||||
@@ -241,7 +357,14 @@ def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
used = collect_used()
|
||||
env_ids = sorted(set(id for ch, id in used if ch != R.CHTAB_WALL)
|
||||
| CLIMB_OVERLAY_ENV_IDS) # + пол-оверлей подъёма
|
||||
| CLIMB_OVERLAY_ENV_IDS # + пол-оверлей подъёма
|
||||
| LOOSE_ANIM_ENV_IDS # + дрожащие кадры loose
|
||||
| SPIKE_ANIM_ENV_IDS # + кадры выдвижения пик
|
||||
| GATE_ANIM_ENV_IDS # + кадры открытия ворот
|
||||
| TORCH_ENV_IDS # + основание факела
|
||||
| STUCK_ENV_IDS # + нажатая кнопка-closer
|
||||
| OPENER_NOFLOOR_ENV_IDS # + opener без пола слева
|
||||
| LEVELDOOR_ENV_IDS) # + дверь уровня (створка/лестница)
|
||||
wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL)
|
||||
|
||||
env_bg = [i for i in env_ids if i not in FORE_ENV_IDS]
|
||||
@@ -250,7 +373,7 @@ def main():
|
||||
# ENV фон -> страницы по id>>SHIFT
|
||||
pages = {}
|
||||
for id in env_bg:
|
||||
w, h, pix = load_indexed(R.CHTAB_ENV, id)
|
||||
w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS)
|
||||
pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
|
||||
max_pg = max(pages) if pages else -1
|
||||
env_page_names = []
|
||||
@@ -278,6 +401,28 @@ def main():
|
||||
total_bytes += sz
|
||||
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
# POTION (chtab_1) -> idx=id: склянки + кадры пузырька + маска
|
||||
# chtab_1 целиком (res151..res173 = id 1..23): пламя факелов, меч-предмет,
|
||||
# склянки зелий. Пузырьки и их маска рисуются MONO-блитом — пакуем
|
||||
# силуэтами в нужном цвете (см. POT_*_COLOR).
|
||||
pot_entries = {}
|
||||
for id in range(1, 24):
|
||||
if id in POT_BUBBLE_IDS:
|
||||
pot_entries[id] = load_potion(id, POT_BUBBLE_COLOR)
|
||||
elif id == POT_MASK_ID:
|
||||
pot_entries[id] = load_potion(id, POT_MASK_COLOR)
|
||||
elif id in POT_FLAME_IDS:
|
||||
# Пламя оригинал рисует blitters_0_no_transp (seg008
|
||||
# draw_tile_anim_right) — непрозрачным боксом поверх всего, что
|
||||
# сзади. Все 9 кадров лежат на одном канвасе 16x18, поэтому
|
||||
# непрозрачный блит сам стирает предыдущий кадр: heal не нужен.
|
||||
pot_entries[id] = load_potion(id, opaque=True)
|
||||
else:
|
||||
pot_entries[id] = load_potion(id)
|
||||
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries)
|
||||
total_bytes += sz
|
||||
print(f" pop_pot.atl (chtab_1: факелы+зелья): {len(pot_entries)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
build_palette(os.path.join(OUT_DIR, "pop_bg.pal"))
|
||||
write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"),
|
||||
env_page_names, "pop_wall.atl", "pop_fore.atl")
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pop_pack_guard.py — упаковщик спрайтов СТРАЖА (chtab_5_guard, база 750).
|
||||
|
||||
Оригинал грузит стражей из отдельного .DAT по типу уровня
|
||||
(GUARD/FAT/SKEL/VIZIER/SHADOW, seg000:1092); уровень 1 — обычный «guard»,
|
||||
то есть data/GUARD/res(750+id).png. Палитра — НЕ из PNG: оригинал
|
||||
подменяет её из res10.bin (guard_palettes, 7 палитр по 16 цветов, 6-бит) по
|
||||
level.guards_color; на уровне 1 у обоих стражей color = 2.
|
||||
|
||||
Выход: poc/res/guard/g0..gN.atl (по 8 спрайтов на страницу, адресация
|
||||
id>>3 / id&7 — как у Kid) + группа палитры 0x90..0x9F в общий kid.pal
|
||||
(его собирает pop_pack_kid.py, см. GUARD_PAL_BASE там).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import pop_pack_bg as B
|
||||
from pop_pack_kid import transpose_cols
|
||||
|
||||
GUARD_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "GUARD")
|
||||
PAL_BIN = os.path.join(HERE, "..", "SDLPoP", "data", "PRINCE", "res10.bin")
|
||||
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "guard")
|
||||
PAL_BASE = 0x90 # слоты палитры Sprinter под стража
|
||||
COLOR = 2 # level.guards_color обоих стражей уровня 1
|
||||
SHIFT = 3 # g[id>>3], idx id&7 (спрайты крупные)
|
||||
MASK = (1 << SHIFT) - 1
|
||||
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
|
||||
|
||||
|
||||
def load_guard(id):
|
||||
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
|
||||
COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
|
||||
|
||||
Off-by-one как у Kid (см. pop_pack_kid.load_kid): load_chtab_from_file
|
||||
зовётся с ресурсом 750, а images[i-1] = res(750+i), то есть images[0] =
|
||||
res751. Рисование берёт get_image(chtab, obj_id+1 − 1) = images[image],
|
||||
значит frame.image=N -> res(751+N). С res(750+N) страж стоял в позе
|
||||
предыдущего кадра (замечено на frame_166_stand_inactive: image 17
|
||||
рисовался как res767 — выпад — вместо res768). """
|
||||
path = os.path.join(GUARD_DIR, f"res{751 + id}.png")
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
im = Image.open(path)
|
||||
if im.mode != "P":
|
||||
B.die(f"res{750+id}: mode {im.mode}, ожидался P")
|
||||
src = im.tobytes()
|
||||
rowmajor = bytes((0xFF if v == 0 else PAL_BASE + v) for v in src)
|
||||
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
|
||||
|
||||
|
||||
def guard_palette():
|
||||
"""16 цветов (r,g,b 0..63) из res10.bin для COLOR."""
|
||||
b = open(PAL_BIN, "rb").read()
|
||||
off = 0x30 * COLOR - 0x30
|
||||
return [(b[off + i * 3], b[off + i * 3 + 1], b[off + i * 3 + 2]) for i in range(16)]
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
pages = {}
|
||||
n = 0
|
||||
for id in range(0, MAX_ID + 1):
|
||||
g = load_guard(id)
|
||||
if g is None:
|
||||
continue
|
||||
pages.setdefault(id >> SHIFT, {})[id & MASK] = g
|
||||
n += 1
|
||||
if not pages:
|
||||
B.die("нет спрайтов стража")
|
||||
total = 0
|
||||
for pg in range(max(pages) + 1):
|
||||
nm = f"g{pg}.atl"
|
||||
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
|
||||
total += sz
|
||||
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б")
|
||||
print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -24,6 +24,12 @@ import pop_pack_bg as B # pack_atlas, blob, scale6to8, ENV_SHIFT
|
||||
KID_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/KID"
|
||||
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
|
||||
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
|
||||
SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F
|
||||
GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
|
||||
# chtab_0 целиком: sword_tbl (seg006:2047) адресует id 0..33 — подъём меча,
|
||||
# ножны И вся боёвка. Меч в оригинале ОДИН на всех: и Кид, и страж рисуют
|
||||
# клинок из этого же chtab_0 (add_sword_to_objtable, seg006:1798).
|
||||
SWORD_IDS = tuple(range(0, 34))
|
||||
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
|
||||
MASK = (1 << SHIFT) - 1
|
||||
|
||||
@@ -62,6 +68,22 @@ def load_kid(aidx):
|
||||
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
|
||||
|
||||
|
||||
def load_sword(id):
|
||||
"""chtab_0 (меч в руке): sword_tbl[].id = N -> res(700+N+1).png в data/PRINCE
|
||||
(в оригинале add_midtable(chtab, obj_id+1) -> get_image(chtab, obj_id)).
|
||||
Пиксели, как у Kid, COLUMN-MAJOR (gfx_blit_cols + бесплатный флип)."""
|
||||
path = os.path.join(os.path.dirname(B.R.VDUNGEON_DIR), B.POT_DIR_NAME,
|
||||
f"res{700 + id + 1}.png")
|
||||
if not os.path.exists(path):
|
||||
B.die(f"нет файла {path}")
|
||||
im = Image.open(path)
|
||||
if im.mode != "P":
|
||||
B.die(f"res{700+id+1}: mode {im.mode}, ожидался P")
|
||||
src = im.tobytes()
|
||||
rowmajor = bytes((0xFF if v == 0 else SWORD_PAL_BASE + v) for v in src)
|
||||
return im.width, im.height, transpose_cols(im.width, im.height, rowmajor)
|
||||
|
||||
|
||||
def _read16(path):
|
||||
b = open(path, "rb").read()
|
||||
return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)]
|
||||
@@ -69,18 +91,35 @@ def _read16(path):
|
||||
|
||||
def build_palette(out_path):
|
||||
"""ЕДИНАЯ игровая палитра (gfx_pal_fload пишет с индекса 0, перезатирая
|
||||
всё — bg и Kid обязаны жить в одном файле): env@0x50, wall@0x60, kid@0x70.
|
||||
всё — bg и Kid обязаны жить в одном файле): chtab_1@0x40 (пламя/зелья),
|
||||
env@0x50, wall@0x60, kid@0x70.
|
||||
"""
|
||||
pal = bytearray(256 * 4)
|
||||
groups = [
|
||||
(0x50, os.path.join(B.R.VDUNGEON_DIR, "res200.pal")), # env
|
||||
(0x60, os.path.join(B.R.VDUNGEON_DIR, "res360.pal")), # wall
|
||||
(PAL_BASE, os.path.join(KID_DIR, "res400.pal")), # kid 0x70
|
||||
(B.POT_PAL_BASE, os.path.join(os.path.dirname(B.R.VDUNGEON_DIR),
|
||||
B.POT_DIR_NAME, "res150.pal")), # chtab_1 0x40
|
||||
(SWORD_PAL_BASE, os.path.join(os.path.dirname(B.R.VDUNGEON_DIR),
|
||||
B.POT_DIR_NAME, "res700.pal")), # chtab_0 0x80
|
||||
]
|
||||
# 16 базовых цветов экрана (mono-блит красит ими: пламя, пузырьки зелья)
|
||||
for i, (r, g, bl) in enumerate(B.VGA16):
|
||||
o = (B.VGA16_PAL_BASE + i) * 4
|
||||
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
||||
for base, path in groups:
|
||||
for i, (r, g, bl) in enumerate(_read16(path)):
|
||||
o = (base + i) * 4
|
||||
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
||||
# Палитра стража берётся НЕ из .pal, а из res10.bin (guard_palettes:
|
||||
# 7 палитр по 16 цветов, 6-бит) по level.guards_color; на уровне 1 у
|
||||
# обоих стражей color = 2. См. toolchain/pop_pack_guard.py.
|
||||
import pop_pack_guard as G
|
||||
for i, (r, g, bl) in enumerate(G.guard_palette()):
|
||||
o = (GUARD_PAL_BASE + i) * 4
|
||||
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(pal)
|
||||
|
||||
@@ -114,6 +153,16 @@ def main():
|
||||
names.append(None)
|
||||
print(f" (kid стр {pg}: пусто)")
|
||||
|
||||
# chtab_0: меч в руке — весь chtab (id 0..33), индекс в атласе = id.
|
||||
sw = {}
|
||||
for i, id in enumerate(SWORD_IDS):
|
||||
w, h, pix = load_sword(id)
|
||||
sw[i] = (w, h, pix)
|
||||
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, "sword.atl"), sw)
|
||||
total += sz
|
||||
print(f" sword.atl (chtab_0, id {SWORD_IDS[0]}..{SWORD_IDS[-1]}): "
|
||||
f"{len(sw)} спрайтов, каталог {cnt}, {sz} Б")
|
||||
|
||||
build_palette(os.path.join(OUT_DIR, "kid.pal"))
|
||||
|
||||
# C-заголовок
|
||||
@@ -126,7 +175,10 @@ def main():
|
||||
f.write("static const char *const kid_atl[KID_PAGES] = {\n")
|
||||
for nm in names:
|
||||
f.write(" %s,\n" % ('"%s"' % nm if nm else "0"))
|
||||
f.write('};\n#define KID_PAL_FILE "kid.pal"\n#endif\n')
|
||||
f.write('};\n#define KID_PAL_FILE "kid.pal"\n')
|
||||
f.write('#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */\n')
|
||||
f.write(f"#define KID_SWORD_ID0 {SWORD_IDS[0]} /* индекс в атласе = sword_tbl.id - ID0 */\n")
|
||||
f.write("#endif\n")
|
||||
|
||||
print(f"ИТОГО: {len(ids)} спрайтов Kid, {len(names)} EMM-страниц, "
|
||||
f"{total} Б диск (+1КБ палитра) -> {OUT_DIR}")
|
||||
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 718 B |
|
After Width: | Height: | Size: 788 B |
|
After Width: | Height: | Size: 526 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 307 B |
|
After Width: | Height: | Size: 498 B |
|
After Width: | Height: | Size: 150 B |
|
After Width: | Height: | Size: 225 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 879 B |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 219 B |
|
After Width: | Height: | Size: 252 B |
|
After Width: | Height: | Size: 717 B |
|
After Width: | Height: | Size: 89 B |
|
After Width: | Height: | Size: 89 B |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 552 B |
|
After Width: | Height: | Size: 700 B |
|
After Width: | Height: | Size: 640 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 422 B |
|
After Width: | Height: | Size: 578 B |
|
After Width: | Height: | Size: 562 B |
|
After Width: | Height: | Size: 163 B |