Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,370 @@
|
||||
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
||||
|
||||
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
|
||||
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
|
||||
— рассчитан на старт с пустого контекста.
|
||||
|
||||
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
||||
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
|
||||
ограничения окна W3.
|
||||
|
||||
---
|
||||
|
||||
## 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,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 20 /* индекс в атласе = 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`.
|
||||
@@ -6,21 +6,32 @@ 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
|
||||
# --w3 берёт ОДИН файл на флаг. В W3-резиденте держим ТОЛЬКО pop_bg —
|
||||
# отрисовку тайлов: она большая и её зовёт лишь главный цикл. Всё
|
||||
# остальное живёт в W1/W2, чтобы оставаться достижимым из будущих
|
||||
# __banked-модулей (стражи): из банка резидентная страница W3 не видна —
|
||||
# ни напрямую, ни транзитивно. См. docs/layout_plan_v2.md §2 (R2), §4.
|
||||
EXTRA_FLAGS ?= --gfx 256 --w3 pop_bg.c
|
||||
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c pop_map.c pop_trob.c pop_redraw.c pop_geom.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
|
||||
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA)
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
||||
@@ -28,7 +39,7 @@ $(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
|
||||
cd $(TC) && python3 pop_pack_bg.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: 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 | недостижима в игре | |
|
||||
@@ -8,449 +8,16 @@
|
||||
|
||||
#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
|
||||
|
||||
#define KID_NSEQ 115
|
||||
static const uint16_t kid_seq_off[KID_NSEQ] = {
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
|
||||
#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)
|
||||
|
||||
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
|
||||
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
|
||||
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
|
||||
@@ -29,6 +41,16 @@ 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);
|
||||
|
||||
/* Задать ряд 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);
|
||||
|
||||
/* Задать ряд 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);
|
||||
|
||||
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
|
||||
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
|
||||
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
|
||||
@@ -36,4 +58,78 @@ void pop_room_draw(uint8_t room_num,
|
||||
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);
|
||||
|
||||
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
|
||||
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
|
||||
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
|
||||
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
|
||||
void pop_fore_heal(void);
|
||||
|
||||
/* 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);
|
||||
void pop_loose_bake_empty(int row, int col);
|
||||
|
||||
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
|
||||
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
|
||||
* Состояние — pop_ceil_modif[10] в pop_map. */
|
||||
void pop_ceil_shake_draw(int col);
|
||||
void pop_ceil_bake_empty(int col);
|
||||
|
||||
/* Кусок приземлился (loose_land): 0 = нет, иначе tilepos+1 тайла, на который
|
||||
* он лёг. Ставит pop_loose_mob_tick, разбирает pop_loose_tick (debris+knock). */
|
||||
extern uint8_t pop_loose_landed;
|
||||
|
||||
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
|
||||
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
|
||||
void pop_spike_redraw(int row, int col);
|
||||
|
||||
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
|
||||
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
|
||||
* (пламя рисуется в ячейке правого соседа). */
|
||||
void pop_torch_draw(int row, int col, uint8_t modif);
|
||||
void pop_potion_draw(int row, int col, uint8_t modif);
|
||||
|
||||
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
|
||||
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
|
||||
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
|
||||
void pop_room_redraw_seam_left(void);
|
||||
|
||||
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
|
||||
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
|
||||
* кнопка нажата/отжата, floor → debris от упавшей плиты. */
|
||||
void pop_floor_bake(int row, int col);
|
||||
void pop_button_redraw(int row, int col); /* = pop_floor_bake (кнопка) */
|
||||
|
||||
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
|
||||
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
|
||||
void pop_leveldoor_redraw(int row, int col);
|
||||
|
||||
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
|
||||
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
|
||||
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
|
||||
void pop_loose_mob_spawn(int row, int col);
|
||||
void pop_loose_mob_tick(void);
|
||||
void pop_loose_mob_reset(void); /* остановить кусок (смена комнаты) */
|
||||
|
||||
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
|
||||
* seg008:1561). Звать после kid_draw, перед fore-слоем. */
|
||||
void pop_loose_mob_draw_over(void);
|
||||
|
||||
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
|
||||
uint8_t pop_loose_mob_pos(int *col, int *y);
|
||||
|
||||
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
|
||||
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
|
||||
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
|
||||
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
|
||||
void pop_clip_sprite(int x, int w, int sy_top, int h);
|
||||
|
||||
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
|
||||
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
|
||||
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
|
||||
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
|
||||
void pop_room_clip_borders(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
#include "pop_kid.h"
|
||||
#include "pop_map.h"
|
||||
|
||||
/* Отладочные клавиши осторожного шага (эмуляция 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
|
||||
#define CONTROL_IGNORE 1
|
||||
@@ -45,6 +51,7 @@
|
||||
#define SEQ_49_STAND_UP_FROM_CROUCH 49
|
||||
#define SEQ_50_CROUCH 50
|
||||
#define SEQ_79_CROUCH_HOP 79
|
||||
#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
|
||||
@@ -98,6 +105,22 @@ 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;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void run_jump(void)
|
||||
{
|
||||
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
|
||||
@@ -185,6 +208,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,6 +220,11 @@ static void control_crouched(void)
|
||||
|
||||
static void control_standing(void)
|
||||
{
|
||||
/* Shift в стойке над предметом — присесть к нему (seg005:0358, первая
|
||||
* строка control_standing). Идёт ДО остальных shift-веток. */
|
||||
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
|
||||
get_item_action())
|
||||
return;
|
||||
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */
|
||||
if (control_shift == CONTROL_HELD) {
|
||||
if (control_backward == CONTROL_HELD) back_pressed();
|
||||
@@ -291,6 +321,16 @@ static void control_jumpup(void)
|
||||
static void control(void)
|
||||
{
|
||||
uint8_t f = Kid.frame;
|
||||
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
|
||||
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
|
||||
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
|
||||
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
|
||||
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
|
||||
* пару пикселей меньше, чем должен»). */
|
||||
if (Kid.action == 5 || Kid.action == 4) { /* bumped / in_freefall */
|
||||
release_arrows();
|
||||
return;
|
||||
}
|
||||
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
|
||||
control_standing();
|
||||
else if (f == FRAME_48_TURN)
|
||||
@@ -314,9 +354,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv)
|
||||
{
|
||||
unsigned long s = *seed * 214013UL + 2531011UL;
|
||||
*seed = s;
|
||||
return (uint16_t)((uint16_t)(s >> 16) % (uint16_t)(maxv + 1));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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) — 32-битный LCG оригинала. Сид ВНЕШНИЙ: у стен
|
||||
* (раскладка кладки) и у анимаций тайлов (фаза факелов) свои
|
||||
* последовательности, смешивать их нельзя. */
|
||||
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv);
|
||||
|
||||
#endif
|
||||
@@ -2,28 +2,50 @@
|
||||
* 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_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;
|
||||
|
||||
/* Координатные таблицы (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
|
||||
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
|
||||
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
||||
int8_t knock;
|
||||
|
||||
|
||||
/* ---- Атласы 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): отдельный
|
||||
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237).
|
||||
* Из sword_tbl взяты только строки 35..42 — те, что адресуют эти кадры (бой
|
||||
* не портирован); индекс в атласе = sword_frame − 35. Смещения таблицы —
|
||||
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */
|
||||
#define SWORD_SF_LO 35
|
||||
#define SWORD_SF_HI 42
|
||||
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 };
|
||||
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 };
|
||||
static atlas_t swordp;
|
||||
static uint8_t sword_ok;
|
||||
|
||||
int pop_sword_load(void)
|
||||
{
|
||||
if (atlas_load(&swordp, "sword.atl") != 0) return -1;
|
||||
sword_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pop_kid_load(uint8_t npages)
|
||||
{
|
||||
static const char *const names[KID_MAXPAGES] = {
|
||||
@@ -50,8 +72,90 @@ 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;
|
||||
}
|
||||
#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 байт вместо таблицы. */
|
||||
static kframe cur_frame;
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
|
||||
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
|
||||
static void load_frame(void)
|
||||
{
|
||||
const uint8_t *f = kd_frame_ptr(Kid.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];
|
||||
}
|
||||
|
||||
static uint8_t char_dx_forward(int8_t dx)
|
||||
{
|
||||
@@ -59,17 +163,23 @@ static uint8_t char_dx_forward(int8_t dx)
|
||||
return (uint8_t)(Kid.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). */
|
||||
/* 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++;
|
||||
@@ -108,20 +218,29 @@ void play_seq(void)
|
||||
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
|
||||
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.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++;
|
||||
break;
|
||||
case 0xF1: /* END_LEVEL */
|
||||
break;
|
||||
case 0xF3: /* GET_ITEM — потребить 1 байт */
|
||||
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
|
||||
* (порт play_seq -> proc_get_object, seg006) */
|
||||
if (SEQ(Kid.curr_seq) == 1)
|
||||
pop_proc_get_object();
|
||||
Kid.curr_seq++;
|
||||
break;
|
||||
case 0xF6: /* DIE nop */
|
||||
break;
|
||||
default: /* КАДР */
|
||||
Kid.frame = cmd;
|
||||
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
|
||||
gfx_w0_unmap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -132,8 +251,8 @@ 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.curr_seq = kid_seq_off[seq_id];
|
||||
@@ -176,23 +295,79 @@ static uint8_t kid_lvalid[2];
|
||||
* 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);
|
||||
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);
|
||||
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе
|
||||
* брызги останутся на экране после кадра. */
|
||||
{
|
||||
uint8_t dp = gfx_get_draw_page();
|
||||
if (kid_lvalid[dp]) {
|
||||
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx;
|
||||
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
|
||||
int x1 = kid_lx[dp] + (int)kid_lw[dp];
|
||||
int y1 = kid_ly[dp] + (int)kid_lh[dp];
|
||||
if (bx + (int)w > x1) x1 = bx + (int)w;
|
||||
if (top + (int)h > y1) y1 = top + (int)h;
|
||||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
||||
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра 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]);
|
||||
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
|
||||
kid_lvalid[p] = 0;
|
||||
}
|
||||
|
||||
void kid_draw(void)
|
||||
{
|
||||
kframe fr = kid_frames[Kid.frame];
|
||||
kframe fr = cur_frame; /* load_frame уже положил кадр (play_seq) */
|
||||
int fwd, obj_x, obj_y, top;
|
||||
uint8_t page, idx;
|
||||
const uint8_t *img;
|
||||
@@ -200,7 +375,10 @@ void kid_draw(void)
|
||||
|
||||
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 +402,73 @@ 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: центрирование */
|
||||
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, оставит след. */
|
||||
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) {
|
||||
uint8_t sf = (uint8_t)(fr.sword & 0x3F);
|
||||
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) {
|
||||
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
|
||||
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
|
||||
uint16_t sw, sh;
|
||||
gfx_w0_map(swordp.page);
|
||||
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
|
||||
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
|
||||
if (sw && sh) {
|
||||
uint8_t sflip = (Kid.direction >= 0);
|
||||
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
|
||||
: (int)SWORD_DX[si]);
|
||||
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
|
||||
uint8_t dp = gfx_get_draw_page() & 1;
|
||||
if (sflip) sbx -= (int)sw;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
if (!kid_lvalid[dp]) {
|
||||
kid_lx[dp] = sbx; kid_ly[dp] = stop;
|
||||
kid_lw[dp] = sw; kid_lh[dp] = sh;
|
||||
kid_lvalid[dp] = 1;
|
||||
} else {
|
||||
int x0 = kid_lx[dp], y0 = kid_ly[dp];
|
||||
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
|
||||
if (sbx < x0) x0 = sbx;
|
||||
if (stop < y0) y0 = stop;
|
||||
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
|
||||
if (stop + (int)sh > y1) y1 = stop + (int)sh;
|
||||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
||||
kid_lw[dp] = (uint16_t)(x1 - x0);
|
||||
kid_lh[dp] = (uint16_t)(y1 - y0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
@@ -26,9 +26,26 @@ typedef struct {
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
/* Инициализировать Kid: последовательность seq_id, клетка col/row,
|
||||
@@ -52,10 +69,22 @@ 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);
|
||||
|
||||
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
|
||||
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
|
||||
void kid_heal(void);
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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
|
||||
#define BP_START 2112u
|
||||
#define ROOM_TILES 30u
|
||||
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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,80 @@
|
||||
/*
|
||||
* 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): комната / поза / направление. */
|
||||
uint8_t pop_level_start_room(void);
|
||||
uint8_t pop_level_start_pos(void);
|
||||
int8_t pop_level_start_dir(void);
|
||||
|
||||
#endif
|
||||
@@ -14,11 +14,91 @@
|
||||
#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);
|
||||
|
||||
/* Установлен в 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);
|
||||
|
||||
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
|
||||
* Звать в enter_room. */
|
||||
void pop_map_set_room(uint8_t room);
|
||||
|
||||
/* 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;
|
||||
void pop_kid_hp_reset(void);
|
||||
|
||||
/* Предметы (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;
|
||||
uint8_t pop_get_item_action(void); /* 0 нет / 1 присесть / 2 поднять меч */
|
||||
void pop_proc_get_object(void); /* эффект — из 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);
|
||||
void pop_loose_reset(void); /* сброс 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);
|
||||
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);
|
||||
|
||||
/* Установлен в 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 +
|
||||
@@ -26,6 +106,13 @@ extern uint8_t pop_fell_out;
|
||||
* Порядок как в оригинале (seg000 play_kid_frame). */
|
||||
void pop_phys_tick(void);
|
||||
|
||||
/* 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);
|
||||
|
||||
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
|
||||
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
|
||||
#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */
|
||||
|
||||
@@ -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,488 @@
|
||||
/*
|
||||
* 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 unsigned long 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;
|
||||
}
|
||||
|
||||
static uint8_t torch_tick; /* делитель скорости анимации пламени */
|
||||
|
||||
void pop_process_trobs(uint8_t cur_room)
|
||||
{
|
||||
uint8_t i, w = 0;
|
||||
torch_tick++;
|
||||
|
||||
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
|
||||
* 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_tick): у нас кадр короче игрового тика
|
||||
* оригинала, и пламя «кипело» вдвое быстрее нужного. */
|
||||
if (room == cur_room && (torch_tick & 1) == 0)
|
||||
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,26 +13,210 @@
|
||||
#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"
|
||||
|
||||
__sfr __at (0xFE) io_border;
|
||||
|
||||
/* 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;
|
||||
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);
|
||||
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
|
||||
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
|
||||
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
|
||||
/* Модификаторы тайлов (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
|
||||
|
||||
if (pop_bg_load() != 0) { /* атласы — ДО initgraph */
|
||||
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
|
||||
puts("pop_bg_load failed");
|
||||
return 1;
|
||||
}
|
||||
if (pop_kid_data_load("kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
|
||||
printf("kid_data.bin load failed\n");
|
||||
return 1;
|
||||
}
|
||||
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
|
||||
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
||||
puts("kid atlas load failed");
|
||||
return 1;
|
||||
}
|
||||
if (pop_level_load("res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */
|
||||
pop_level_load("a:\\res2001.bin") != 0) {
|
||||
puts("level load failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
initgraph();
|
||||
gfx_set_visible_page(0);
|
||||
@@ -44,18 +228,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 +253,9 @@ int main(void)
|
||||
while (!kbd_raw_down(KBD_ESC)) {
|
||||
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||
* переключ). Выключение возвращает показ на страницу 0. */
|
||||
|
||||
io_border = 2; /* синий - начало */
|
||||
|
||||
uint8_t sp = kbd_raw_down(KBD_SPACE);
|
||||
if (sp && !sp_prev) {
|
||||
dbuf ^= 1;
|
||||
@@ -86,31 +272,213 @@ int main(void)
|
||||
if (kbd_raw_down(0x16)) frozen = 1;
|
||||
if (kbd_raw_down(0x1E)) frozen = 0;
|
||||
if (frozen) {
|
||||
io_border = 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_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
|
||||
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
|
||||
kid_tick(); /* play_seq: следующий кадр */
|
||||
pop_phys_tick(); /* K3: падение/приземление/стена */
|
||||
if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */
|
||||
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();
|
||||
}
|
||||
kid_draw(); /* нарисовать новый кадр */
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
io_border = 4; /* зеленый */
|
||||
|
||||
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
|
||||
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
|
||||
io_border = 6; /* голубой */
|
||||
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
|
||||
/* process_trobs — рисования внутри нет) */
|
||||
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
|
||||
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
|
||||
io_border = 4; /* синий */
|
||||
{ /* ворота соседа слева (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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
io_border = 2; /* зеленый */
|
||||
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();
|
||||
}
|
||||
}
|
||||
io_border = 6; /* голубой */
|
||||
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;
|
||||
io_border = 2; /* синий */
|
||||
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_bg(255, 255, 85); /* color_14_brightyellow */
|
||||
pop_flash_time--;
|
||||
flash_on = 1;
|
||||
} else if (pop_kid_hurt) {
|
||||
flash_bg(255, 85, 85); /* color_12_brightred */
|
||||
flash_on = 1;
|
||||
}
|
||||
io_border = 4; /* зеленый */
|
||||
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
|
||||
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
|
||||
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
|
||||
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
|
||||
kid_draw_splash();
|
||||
pop_kid_hurt = 0;
|
||||
}
|
||||
io_border = 0; /* черный */
|
||||
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 паттерн). */
|
||||
|
||||
io_border = 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 +491,7 @@ int main(void)
|
||||
closegraph();
|
||||
pop_bg_free();
|
||||
pop_kid_free();
|
||||
pop_kid_data_free();
|
||||
pop_level_free();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import os
|
||||
import re
|
||||
|
||||
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/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
|
||||
@@ -74,22 +76,21 @@ 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\n" % (len(frames) * 5))
|
||||
|
||||
# seq offsets
|
||||
f.write("#define KID_NSEQ %d\n" % len(offs))
|
||||
@@ -97,6 +98,15 @@ def main():
|
||||
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
|
||||
f.write("#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))
|
||||
print(f"kid_data.bin: {len(frames)*5 + len(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,10 +195,35 @@ def load_indexed(chtab, id):
|
||||
if im.mode != "P":
|
||||
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
|
||||
src = im.tobytes()
|
||||
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
|
||||
|
||||
|
||||
def blob(w, h, pix):
|
||||
"""getimage-блоб: u16 w, u16 h (LE) + пиксели."""
|
||||
return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix
|
||||
@@ -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")
|
||||
|
||||
@@ -24,6 +24,8 @@ 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
|
||||
SWORD_IDS = tuple(range(20, 28)) # sword_tbl для кадров 229..236 (найти-убрать)
|
||||
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
|
||||
MASK = (1 << SHIFT) - 1
|
||||
|
||||
@@ -62,6 +64,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,14 +87,23 @@ 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
|
||||
@@ -114,6 +141,17 @@ def main():
|
||||
names.append(None)
|
||||
print(f" (kid стр {pg}: пусто)")
|
||||
|
||||
# chtab_0: меч в руке. Пакуем ТОЛЬКО кадры подъёма/ножен (sword_tbl
|
||||
# 35..42 -> id 20..27); индекс в атласе = id - 20 (бой не портирован).
|
||||
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 +164,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 |
|
After Width: | Height: | Size: 195 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 89 B |
|
After Width: | Height: | Size: 89 B |
|
After Width: | Height: | Size: 195 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 222 B |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 296 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 223 B |
|
After Width: | Height: | Size: 296 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 208 B |