From 14190f0210e37c944f922a88d71954bc8e28be37 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sat, 8 Aug 2026 16:28:02 +0300 Subject: [PATCH] =?UTF-8?q?MEM-BANK2=20=D1=88=D0=B0=D0=B3=203:=20=D1=85?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B4=D0=BD=D0=B0=D1=8F=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D0=BD=D0=B0=20=D1=81=D0=BB=D0=BE=D1=8F=20?= =?UTF-8?q?=D1=84=D0=BE=D0=BD=D0=B0=20=D0=B2=20=D0=B1=D0=B0=D0=BD=D0=BA=20?= =?UTF-8?q?7=20(90.4%=20->=2035.8%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pop_bg.c разрезан по ЧАСТОТЕ вызова, а не по размеру: pop_bg.c (банк 2) горячее fore-проход, оверлеи, кладка, клип pop_room.c (банк 7) холодное draw_tile, точечные перерисовки, mob, загрузка атласов Стык — три тонкие __banked-обёртки (wall_pattern, wall_pattern_reset, draw_gate_back): тела остаются непомеченными, поэтому горячий fore-проход, зовущий wall_pattern до девяти раз за кадр, платит ноль, а трамплин достаётся только холодному пути — ~40 вызовов на вход в комнату, 26 000 тактов = 0.06 кадра РАЗОВО. Общее состояние (pop_loose_modif, pop_ceil_modif, obj_row/obj_col) писучее, лежит в _DATA/W2 и видно обеим половинам. Заодно удалена мёртвая potion_bubble (169 Б). Грабля: n_banks объявляет само приложение (roomtest.c), а не sprinter-cc. Восьмой банк без правки константы линкуется молча, _bank_pages[7] остаётся 0xFF, и программа встаёт намертво до первого кадра. Диагноз снят дампом _bank_pages из MAME. Замер (ALLOCS=3000): BANK2 11 942 -> 5 872, BANK7 6 035; _CODE и куча не тронуты (22 556 / 4 301). Проверено: tests-host зелёные; построчная сверка pop_bg.c+pop_room.c против дорефакторного pop_bg.c — ни одной строки логики не пропало; 8 комнат в MAME до/после совпали попиксельно по активному экрану (различия только в фазе анимации факелов и кадре Кида); живой прогон с переходами комнат, боем и воротами сверен контрольным запуском HEAD-бинаря. DRAW-COST поднят в приоритете: замер пользователя (ур.1 комната 3, страж убит, Кид стоит) — синяя 80%, зелёная 20%, циан 110%, итого ~210 % кадрового периода В ПОКОЕ. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/layout_plan_v2.md | 33 +- applications/PoP/roomtest/CLAUDE.md | 4 +- applications/PoP/roomtest/Makefile | 4 +- applications/PoP/roomtest/TASKS_CLOSED.md | 117 +++ applications/PoP/roomtest/TASKS_OPEN.md | 108 +-- applications/PoP/roomtest/_pop_bg.h | 49 ++ applications/PoP/roomtest/pop_bg.c | 868 +--------------------- applications/PoP/roomtest/pop_room.c | 846 +++++++++++++++++++++ applications/PoP/roomtest/roomtest.c | 16 +- 9 files changed, 1116 insertions(+), 929 deletions(-) create mode 100644 applications/PoP/roomtest/_pop_bg.h create mode 100644 applications/PoP/roomtest/pop_room.c diff --git a/applications/PoP/docs/layout_plan_v2.md b/applications/PoP/docs/layout_plan_v2.md index 3534add..4eb7223 100644 --- a/applications/PoP/docs/layout_plan_v2.md +++ b/applications/PoP/docs/layout_plan_v2.md @@ -1,12 +1,23 @@ # roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам -> **Замер 2026-08-08 (актуальная сборка, ALLOCS=3000).** `_CODE` 20 524 Б, -> данные 4 375, куча 6 333. Банки: 1 (`guards.c`) 2 519, 2 (`pop_bg.c`) -> 15 045, 3 (`pop_map.c`) 8 713, 4 (`pop_cdraw.c`) 5 905, 5 (`pop_ctrl.c`) -> 2 270, 6 (`pop_trob.c`) 3 119 — все из 16 384. Резидент похудел на 4.3 КБ -> после DRAW-CHAR (отрисовка персонажей уехала из W1 в банк 4 и слилась в -> одну на всех Char); банк 2 при этом почти не разгрузился — свободно -> 1 339 Б. +> **Замер 2026-08-08, после MEM-BANK2 (актуальная сборка, ALLOCS=3000).** +> `_CODE` 22 556 Б, данные 4 375, куча 4 301. Банки: 1 (`guards.c`) 2 289, +> 2 (`pop_bg.c`, ГОРЯЧАЯ половина фона) 5 872, 3 (`pop_map.c`) 8 483, +> 4 (`pop_cdraw.c`) 5 664, 5 (`pop_ctrl.c`) 2 040, 6 (`pop_trob.c`) 2 889, +> 7 (`pop_room.c`, ХОЛОДНАЯ половина фона) 6 035 — все из 16 384. +> +> Слой фона разрезан надвое по ЧАСТОТЕ ВЫЗОВА (`TASKS_CLOSED.md#mem-bank2`): +> общие листья — в резидент `pop_tile.c` (W1 замаплен всегда, таблицы видны +> обеим половинам), горячий fore-проход остался в банке 2, холодная отрисовка +> комнаты и точечные перерисовки уехали в банк 7. Стык — три тонкие +> `__banked`-обёртки, чтобы трамплин платил только холодный путь. +> **Банк 2: 90.4 % -> 35.8 %.** Резидент вырос на 2 КБ (листья) — куча +> 6 333 -> 4 301 Б, это плата за то, что таблицы должны быть видны из двух +> банков. +> +> Число банков дублируется: `--bank N=` в Makefile И `const uint8_t n_banks` +> в `roomtest.c`. Расхождение — не ошибка сборки, а зависание до первого +> кадра. > > **Замер 2026-08-01 (историческая отметка).** `_CODE` 25 119 Б, `_DATA` 3 709, > куча ~2.4 КБ. Банки: 1 (`guards.c`) 1 896, 2 (`pop_bg.c`) 13 792, @@ -17,10 +28,10 @@ > вынесена из банка 2 в собственный банк 4, потому что банк 2 подошёл к > потолку (16 021 из 16 384) — коммит `2f3e854`. > -> **Свободного места в банке 2 осталось ~2.6 КБ**, а туда же просятся -> чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать -> `levels_plan.md` §3 — посчитать, куда это ляжет. Свободные номера банков -> есть, гранулярность — файл. +> **Свободного места в банке 2 теперь 10.5 КБ**, в банке 7 — 10.3 КБ; туда +> просятся чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать +> `levels_plan.md` §3 — всё равно посчитать, куда это ляжет. Следующий +> свободный номер банка — 8, гранулярность — файл. > > Из плана ниже **не сделаны шаги 5 (данные: `room_modif`, `dl1/dl2`, > `_kbdraw_down`; потенциал ~1.5 КБ) и 7 (дедуп `draw_tile`, отложен по diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index c3e3df6..d9b883a 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -61,7 +61,9 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`). |------|--------------------| | `roomtest.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), tick→draw порядок слоёв. | | `pop_tile.c/.h` | Общие ЛИСТЬЯ слоя фона — РЕЗИДЕНТ W1: блит куска атласа, чтение тайла комнаты, `tile_table`. Резидент потому, что их зовут обе половины слоя фона из разных банков, а `const`-таблицы банка из чужого банка не видны. | -| `pop_bg.c/.h` | Отрисовка фона композицией тайлов (`seg008` draw_tile/wall_pattern), fore-окклюзия поверх ЛЮБОГО персонажа (`seg003` redraw_at_char/char2 — один проход на всех Char), loose-полы (shake/bake/mob, `seg007/008`). | +| `pop_bg.c/.h` | ГОРЯЧАЯ половина слоя фона (банк 2, каждый кадр): fore-окклюзия поверх ЛЮБОГО персонажа (`seg003` redraw_at_char/char2 — один проход на всех Char), оверлеи кромки, кладка стены (`wall_pattern`), клип. `pop_bg.h` — публичный API ВСЕГО слоя (обеих половин). | +| `pop_room.c` | ХОЛОДНАЯ половина слоя фона (банк 7, раз на комнату/событие): полная отрисовка комнаты (`seg008` draw_tile), точечные перерисовки тайлов (пики, ворота, кнопки, дверь уровня, дрожащие плиты), падающие куски loose (`seg007` mob), загрузка атласов. | +| `_pop_bg.h` | Внутренний контракт между половинами: общее состояние + три тонкие `__banked`-обёртки. Там же расписано, почему рез прошёл именно здесь и сколько стоит. | | `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. | | `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`, `seg008:22F0/2324`): спрайт, clip_char, брызги, клинок, heal; + полоса HP и палитры соперника. | | `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх ``. | diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index a2a17e3..050fc42 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 4=pop_cdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS) +EXTRA_FLAGS ?= --gfx 256 --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте @@ -105,4 +105,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py -$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) +$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.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) diff --git a/applications/PoP/roomtest/TASKS_CLOSED.md b/applications/PoP/roomtest/TASKS_CLOSED.md index e67233e..203f01a 100644 --- a/applications/PoP/roomtest/TASKS_CLOSED.md +++ b/applications/PoP/roomtest/TASKS_CLOSED.md @@ -322,6 +322,123 @@ BANK4 pop_cdraw 4 625 5 905 +1 280 (сюда переехала отрис +### MEM-BANK2. Разгрузка банка 2 — **ЗАКРЫТА 2026-08-08** + +Банк 2 (`pop_bg.c`) упирался в потолок: 90.8 % ещё до чомперов. Разбор по +символам показал, что он держит ДВЕ разные вещи — горячий fore-проход (каждый +кадр) и холодную отрисовку тайлов (вход в комнату, точечные перерисовки), — +а общие «листья» (`blit_b`, `tile_code`, `tile_table`, `wall_pattern`) нужны +обеим. + +**Ключевое ограничение платформы** (записано в memory +`sdcc_banked_call_rules`): писучие данные банка лежат в `_DATA`/W2 и видны +всем, а `const`-таблицы — в СТРАНИЦЕ банка, и из другого банка их не +прочитать вовсе. Плюс трамплин выбирается ОБЪЯВЛЕНИЕМ: пометить лист +`__banked` — значит заставить платить и горячих вызывающих (654 такта на +круг). + +**Шаг 1 — общие листья в РЕЗИДЕНТ** (`pop_tile.c` + внутренний `pop_tile.h`). +W1 замаплен всегда, поэтому и горячая половина, и будущая холодная зовут их +прямым `call` и читают таблицы напрямую — без дублей и без трамплинов. +Уехали: `blit_b`, `env_b/wall_b/fore_b/pot_b`, `tile_code/tile_mod/ +tile_code_drawn/wall_modifier`, `heal_off`, `get_loose_frame`, +`potion_flask`, `pop_fore_set_clip`, `pop_room_set_below/above` + таблицы +`tile_table`, `COL_XH`, `WALL_FRAM_*`, `SPIKES_FRAM_LEFT`, +`LOOSE_FRAM_LEFT/BOTTOM`. Заодно три функции перестали быть `__banked` — +минус трамплин на кадр. + +**Шаг 2 — дедуп внутри банка.** `wall_pattern` (808 → 394) и `wall_rnd` +(786 → 654): четыре ветки по виду стены (SWS/SWW/WWS/WWW) отличались ТОЛЬКО +набором кусков и числами в одной и той же серии `prandom` — сведены к +таблицам `WP_PARTS` и `WR_RULE`. Порядок вызовов `prandom` сохранён +дословно (иначе разъедется раскладка кладки). + +**Грабля, пойманная замером:** после выноса `tile_table` из `static const` в +глобальную SDCC перестала сворачивать `tile_table[10].fore_x/fore_y` в +константы и стала читать их в рантайме — `potion_flask` раздулся 115 → 265 Б. +Лечится макросами, которыми инициализируется та же запись таблицы (одно место +правки, дублирования данных нет): 265 → 230. + +**Замер (ALLOCS=3000):** + +``` + было после ш.1 после ш.2 +BANK2 14 815 12 460 11 942 (90.4 % -> 72.9 %, свободно 4442 Б) +_CODE 20 064 22 591 22 556 (куча 6793 -> 4301 Б) +``` + +С релизным `ALLOCS=100000` банк 2 будет ещё примерно на 500-600 Б меньше +(замер до разгрузки: 14 815 -> 14 176). + +**Проверено:** `tests-host` зелёные; в MAME комната 1 уровня 1 после дедупа +совпала с дорефакторным снимком **попиксельно (0 различий из 227 520)**, +комната 3 (сплошная кладка, все четыре вида стены и марки) — та же раскладка +кирпича. + +**Шаг 3 — ХОЛОДНАЯ половина в банк 7** (`pop_room.c` + внутренний +`_pop_bg.h`). Граница проведена по частоте вызова, а не по размеру: + +``` +pop_bg.c (банк 2) ГОРЯЧЕЕ fore-проход, оверлеи, кладка, клип каждый кадр +pop_room.c (банк 7) ХОЛОДНОЕ draw_tile, точечные перерисовки, mob, раз на комнату + загрузка атласов / на событие +``` + +Обратных зависимостей нет вовсе: холодная половина зовёт горячую ровно в +трёх местах (`wall_pattern`, `wall_pattern_reset`, `draw_gate_back`) — и +через ТОНКИЕ `__banked`-обёртки, а не пометкой самих тел. Тела остаются +непомеченными, поэтому fore-проход, который зовёт `wall_pattern` до девяти +раз за кадр, платит ноль. Общее состояние (`pop_loose_modif`, +`pop_ceil_modif`, `obj_row/obj_col`) — писучее, лежит в `_DATA`/W2 и видно +обеим половинам; `obj_row/obj_col` для этого перестали быть `static`. + +**Цена трамплинов:** полная отрисовка комнаты — ~40 вызовов через границу +(по два `wall_pattern` на стенной тайл), 26 000 тактов = 0.06 кадра РАЗОВО +на вход в комнату. Анимация пик/ворот — 2-4 вызова на кадр, ~1 %. Ровно +тот порядок, который был признан приемлемым при постановке задачи. + +Заодно удалена мёртвая `potion_bubble` (169 Б): её работу давно делают +`pop_potion_flask` (резидент) и `pop_potion_draw`, вызывающих не осталось. + +**ГРАБЛЯ, стоившая прогона: `n_banks` объявляет САМО ПРИЛОЖЕНИЕ** +(`roomtest.c`), а не `sprinter-cc`. Восьмой банк без правки этой константы +собирается и линкуется без единого предупреждения, `_bank_pages[7]` +остаётся 0xFF, и трамплин уводит в мусор: exe грузится, но программа встаёт +намертво ещё до первого кадра — на экране остаётся приглашение DSS. +Диагноз снят дампом `_bank_pages` из MAME (`FF EC ED EE EF F0 F1 FF` — +семь страниц вместо восьми), а не гаданием. Предупреждение об этом в +шапке `roomtest.c` было — и всё равно не сработало, потому что число банков +живёт в двух местах. + +**Замер (ALLOCS=3000):** + +``` + было ш.1 ш.2 ш.3 +BANK2 14 815 12 460 11 942 5 872 (90.4 % -> 35.8 %) +BANK7 — — — 6 035 (36.8 %) +_CODE 20 064 22 591 22 556 22 556 (куча 4301 Б — не тронута) +``` + +**Проверено (шаг 3):** +- `tests-host` зелёные; +- построчная сверка `pop_bg.c` + `pop_room.c` против дорефакторного + `pop_bg.c`: НИ ОДНОЙ строки логики не пропало и не изменилось — + расхождения только шапки, включения, три обёртки и удалённый мёртвый код; +- в MAME **8 комнат** (стартовая + 7 по `ROOMNAV`) сняты до и после и сверены + попиксельно по активному экрану 320×256: различия ТОЛЬКО в фазе анимации + (пламя факелов, кадр Кида), статическая кладка совпадает пиксель в пиксель; +- живой прогон: переход между комнатами, бой со стражем, решётка ворот, + окклюзия Кида ближней колонной — как на дорефакторном билде (сверено + контрольным запуском HEAD-бинаря с тем же вводом). + +**Что осталось в запасе, если места снова не хватит** (не делаем, пока нет +причины): вынести из РЕЗИДЕНТА холодные части — инициализацию в `main` + +`enter_room_side`/`pop_start_level` (~1.7 КБ), загрузчик атласов Кида +(~1.2 КБ), `pop_room_load`/`pop_level_load` (~1.4 КБ). Итого ~4-4.5 КБ +резидента, то есть куча 4301 -> ~8.6 КБ. Целиком `pop_level.c` в банк +нельзя: его `pop_level_tile` зовётся из банка на КАЖДЫЙ тайл — только +расщепление модуля. + ### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01** > **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту, diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 578f3e1..0f409ec 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -17,13 +17,14 @@ Уровень 3: **скелет сделан и проверен в MAME 2026-08-07** (бой, падение в пропасть, окклюзия), чомперов ещё нет. -**Разгрузка банка 2 сделана 2026-08-08** ([MEM-BANK2](#mem-bank2)): 90.4 % → -**72.9 %, свободно 4442 Б** — чомперам места хватает. +**Разгрузка банка 2 сделана 2026-08-08** ([MEM-BANK2](TASKS_CLOSED.md#mem-bank2)): 90.4 % → +**35.8 %, свободно 10 512 Б** — холодная половина уехала в банк 7 +(`pop_room.c`), чомперам места с запасом. **DRAW-CHAR сделана и ПРОВЕРЕНА 2026-08-08** (протокол и замеры — [`TASKS_CLOSED.md`](TASKS_CLOSED.md#draw-char)): отрисовка теперь одна на всех `Char`. Банку 2 она дала всего +265 Б — место под чомперов дала уже -[MEM-BANK2](#mem-bank2). +[MEM-BANK2](TASKS_CLOSED.md#mem-bank2). Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга; диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не @@ -50,10 +51,11 @@ |---|--------|-----|-----------| | 1 | [GUARD-PHYS](#guard-phys) | физика стража = физика Кида (одна над `Char`) | **ядро сделано 2026-08-07**; открыт живой сценарий в MAME | | — | [DRAW-CHAR](TASKS_CLOSED.md#draw-char) | отрисовка одна на всех `Char` — **закрыта и проверена 2026-08-08** | — | -| — | [MEM-BANK2](#mem-bank2) | разгрузка банка 2 — **сделана 2026-08-08**: 90.4 % → 72.9 %, свободно 4442 Б | — | +| — | [MEM-BANK2](TASKS_CLOSED.md#mem-bank2) | разгрузка банка 2 — **закрыта 2026-08-08**: 90.4 % → 35.8 %, свободно 10 512 Б | — | | 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 | | — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — | | 3 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели | +| — | [DRAW-COST](#draw-cost) | **кадр не укладывается в бюджет: ~210 % кадрового периода в ПОКОЕ** (замер 2026-08-08) | плавность на ВСЕХ уровнях | | — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | | — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | @@ -184,66 +186,6 @@ скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ живой страж — переход не происходит. -### MEM-BANK2. Разгрузка банка 2 — **шаги 1-2 сделаны 2026-08-08** - -Банк 2 (`pop_bg.c`) упирался в потолок: 90.8 % ещё до чомперов. Разбор по -символам показал, что он держит ДВЕ разные вещи — горячий fore-проход (каждый -кадр) и холодную отрисовку тайлов (вход в комнату, точечные перерисовки), — -а общие «листья» (`blit_b`, `tile_code`, `tile_table`, `wall_pattern`) нужны -обеим. - -**Ключевое ограничение платформы** (записано в memory -`sdcc_banked_call_rules`): писучие данные банка лежат в `_DATA`/W2 и видны -всем, а `const`-таблицы — в СТРАНИЦЕ банка, и из другого банка их не -прочитать вовсе. Плюс трамплин выбирается ОБЪЯВЛЕНИЕМ: пометить лист -`__banked` — значит заставить платить и горячих вызывающих (654 такта на -круг). - -**Шаг 1 — общие листья в РЕЗИДЕНТ** (`pop_tile.c` + внутренний `pop_tile.h`). -W1 замаплен всегда, поэтому и горячая половина, и будущая холодная зовут их -прямым `call` и читают таблицы напрямую — без дублей и без трамплинов. -Уехали: `blit_b`, `env_b/wall_b/fore_b/pot_b`, `tile_code/tile_mod/ -tile_code_drawn/wall_modifier`, `heal_off`, `get_loose_frame`, -`potion_flask`, `pop_fore_set_clip`, `pop_room_set_below/above` + таблицы -`tile_table`, `COL_XH`, `WALL_FRAM_*`, `SPIKES_FRAM_LEFT`, -`LOOSE_FRAM_LEFT/BOTTOM`. Заодно три функции перестали быть `__banked` — -минус трамплин на кадр. - -**Шаг 2 — дедуп внутри банка.** `wall_pattern` (808 → 394) и `wall_rnd` -(786 → 654): четыре ветки по виду стены (SWS/SWW/WWS/WWW) отличались ТОЛЬКО -набором кусков и числами в одной и той же серии `prandom` — сведены к -таблицам `WP_PARTS` и `WR_RULE`. Порядок вызовов `prandom` сохранён -дословно (иначе разъедется раскладка кладки). - -**Грабля, пойманная замером:** после выноса `tile_table` из `static const` в -глобальную SDCC перестала сворачивать `tile_table[10].fore_x/fore_y` в -константы и стала читать их в рантайме — `potion_flask` раздулся 115 → 265 Б. -Лечится макросами, которыми инициализируется та же запись таблицы (одно место -правки, дублирования данных нет): 265 → 230. - -**Замер (ALLOCS=3000):** - -``` - было после ш.1 после ш.2 -BANK2 14 815 12 460 11 942 (90.4 % -> 72.9 %, свободно 4442 Б) -_CODE 20 064 22 591 22 556 (куча 6793 -> 4301 Б) -``` - -С релизным `ALLOCS=100000` банк 2 будет ещё примерно на 500-600 Б меньше -(замер до разгрузки: 14 815 -> 14 176). - -**Проверено:** `tests-host` зелёные; в MAME комната 1 уровня 1 после дедупа -совпала с дорефакторным снимком **попиксельно (0 различий из 227 520)**, -комната 3 (сплошная кладка, все четыре вида стены и марки) — та же раскладка -кирпича. - -**Что осталось в запасе, если места снова не хватит** (не делаем, пока нет -причины): вынести ХОЛОДНУЮ половину (`draw_tile` 1800 + точечные -перерисовки 1438 + падающие плиты 1898 ≈ 5.1 КБ) в свободный банк 7. Теперь -это дёшево: листья уже в резиденте, а `wall_pattern`/`wall_rnd`/марки/ -`draw_gate_back` нужны обеим половинам — им понадобится тонкая `__banked`- -обёртка (тело остаётся непомеченным, горячие вызывающие платят ноль). - ### L3-CHOMP. Чомперы — механика уровня 3 **Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти @@ -254,10 +196,12 @@ _CODE 20 064 22 591 22 556 (куча 6793 -> 4301 Б) коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`). -**Место в банке 2 расчищено** ([MEM-BANK2](#mem-bank2), 2026-08-08): 72.9 %, -**свободно 4442 Б** (было 1074 Б). Считать место ДО кодинга всё равно -обязательно (`levels_plan.md` §5.1), и если не хватит — в запасе вынос -холодной половины `pop_bg` в банк 7, там расписано как. +**Место в банке 2 расчищено** ([MEM-BANK2](TASKS_CLOSED.md#mem-bank2), 2026-08-08): +35.8 %, **свободно 10 512 Б** (было 1074 Б) — холодная половина слоя фона +уехала в банк 7 (`pop_room.c`). Считать место ДО кодинга всё равно +обязательно (`levels_plan.md` §5.1); чомпер — это анимация тайла (холодная +перерисовка), поэтому его отрисовка ложится в `pop_room.c`, а состояние — +в `room_modif`, как у пик и ворот. **Ассеты УЖЕ упакованы (2026-08-07):** `pop_pack_bg.py` кладёт в атлас весь набор кадров чомпера явно (`CHOMPER_BOT_IDS` 101-105, `CHOMPER_TOP_IDS` @@ -372,10 +316,32 @@ offset 22790, **240 байт** = 5 палитр × 16 цветов × 3 байт таблицу нельзя передавать прямо из банка (0xC000+), надо копировать в W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)). -### DRAW-COST. Стоимость fore-прохода после DRAW-CHAR +### DRAW-COST. Кадр НЕ УКЛАДЫВАЕТСЯ в бюджет — нужна оптимизация -**Наблюдение пользователя на приёмке 2026-08-08:** циан-полоса профиля -(спрайты + fore) выросла — «в пределах, но оптимизация отдельной задачей». +> **ЗАМЕР ПОЛЬЗОВАТЕЛЯ 2026-08-08 (после MEM-BANK2).** Уровень 1, комната 3, +> страж УБИТ, Кид СТОИТ (то есть это НЕ худший случай — ни боя, ни движения): +> +> | полоса | фаза | доля кадрового периода | +> |---|---|---| +> | синяя (`PROF(2)`) | ввод + heal + логика | ~80 % | +> | зелёная (`PROF(4)`) | слой фона (loose/ловушки) | ~20 % | +> | циан (`PROF(6)`) | спрайты + fore-проход | **~110 %** | +> +> **Итого ~210 % — больше ДВУХ кадровых периодов на кадр.** Это много; в +> покое должно оставаться место под бой, движение и вторую фигуру. +> Приоритет задачи поднят: это уже не косметика профиля. + +Мерить в ЭТОЙ точке (комната 3, страж убит, Кид стоит) — она воспроизводима +и даёт нижнюю границу; худший случай (бой + бег + падающая плита) считать +отдельно. Метод — memory `z80_profiling_method` (брейкпоинты с +`totalcycles`), полосы бордюра дают только раскладку по фазам. + +Обратить внимание: **синие 80 % при стоящем Киде** — это подозрительно +много для «ввод + heal + логика», heal там наверняка чистит больше, чем +нужно. Разобрать синюю фазу отдельно от циана. + +**Наблюдение пользователя на приёмке DRAW-CHAR 2026-08-08:** циан-полоса +профиля (спрайты + fore) выросла — тогда «в пределах». **Отчего именно.** Раньше перебор тайлов у Кида шёл строго по футпринту кадра (`char_x_left/right`, seg006:1021), а он УЖЕ спрайта; расширение diff --git a/applications/PoP/roomtest/_pop_bg.h b/applications/PoP/roomtest/_pop_bg.h new file mode 100644 index 0000000..98d674a --- /dev/null +++ b/applications/PoP/roomtest/_pop_bg.h @@ -0,0 +1,49 @@ +/* + * _pop_bg.h — ВНУТРЕННИЙ контракт между двумя половинами слоя фона. + * Публичный API слоя — pop_bg.h; общие листья (резидент) — pop_tile.h. + * + * Слой фона разрезан на два банка: + * pop_bg.c (банк 2) — ГОРЯЧАЯ половина: fore-проход поверх персонажей, + * оверлеи, клип. Работает каждый кадр. + * pop_room.c (банк 7) — ХОЛОДНАЯ: полная отрисовка комнаты (draw_tile), + * точечные перерисовки тайлов, падающие плиты, + * загрузка атласов. Раз на комнату/событие. + * + * Резать пришлось потому, что банк 2 упирался в 16 КБ страницы (90.4 % до + * разгрузки), а чомперам и остальным ловушкам уровня 3 место нужно. Граница + * проведена по частоте вызова: холодная половина платит трамплин (~654 такта) + * за каждый вызов сюда, и на входе в комнату таких вызовов ~40 — 26 000 + * тактов, шестая часть кадрового бюджета РАЗОВО. Для горячего пути цена + * нулевая: он зовёт свои функции прямо, а общие листья — из резидента. + * + * Что осталось в банке 2 и вынесено сюда обёртками: кладка стены + * (wall_pattern — её зовёт и fore-проход каждый кадр) и решётка ворот + * соседней комнаты (draw_gate_back — её зовёт шовная окклюзия). Обратных + * зависимостей (горячая половина → холодная) нет вовсе. + */ +#ifndef _POP_BG_H +#define _POP_BG_H + +#include + +/* Состояние дрожащих плит — живёт в pop_map, читают обе половины. */ +extern uint8_t pop_loose_modif[30]; +extern uint8_t pop_ceil_modif[10]; /* плита-потолок: loose ряда 2 комнаты сверху */ + +/* Видимая высота полосы кладки у потолка (room-space y 0..7): её печёт + * холодная половина, а поверх персонажа кладёт горячая. */ +#define CEIL_BAND_H 8 + +/* Тайл ОБЪЕКТА активного персонажа (set_objtile_at_char, seg006:1833) — в + * момент обработки этого тайла спрайт попадает в midtable. Задаёт горячая + * половина (pop_fore_over_char), читают other_overlay_tile (там же) и + * порядок «плита vs Kid» в mob_tick_one (холодная). Писучие данные банка + * лежат в _DATA/W2 и видны из любого банка — см. sdcc_banked_call_rules. */ +extern int8_t pop_bg_obj_col, pop_bg_obj_row; + +/* ---- Точки входа ГОРЯЧЕЙ половины для холодной (через трамплин) ------- */ +void pop_wall_pattern_b(int row, int col, int which_part) __banked; +void pop_wall_pattern_reset_b(void) __banked; +void pop_gate_back_b(uint8_t modl, int xh, int dby, int dmy) __banked; + +#endif diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index bdd93b5..79a7ccc 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -1,12 +1,12 @@ /* - * pop_bg.c — статический фон комнаты PoP композицией слоёв (порт - * applications/PoP/toolchain/render_room.py -> seg008.c draw_tile/ - * wall_pattern). Подробности алгоритма — в render_room.py и - * memory/pop_background_strategy. + * pop_bg.c — ГОРЯЧАЯ половина слоя фона PoP: то, что работает КАЖДЫЙ кадр — + * передний слой поверх персонажей (порт seg003 redraw_at_char + seg008 + * set_char_collision), оверлеи кромки, кладка стены и клип. Полная + * отрисовка комнаты и точечные перерисовки уехали в pop_room.c (банк 7); + * граница реза и её цена расписаны в _pop_bg.h. * - * Только DUNGEON-стиль (Фаза 1). Анимация (пламя факелов, шипы, дверь- - * плита, сама решётка ворот) НЕ рисуется — отдельными sprite_t поверх - * (Фаза 2). Решётка ворот из ЛЕВОЙ комнаты рисуется статичным кадром. + * Подробности алгоритма кладки — в toolchain/render_room.py и + * memory/pop_background_strategy. Только DUNGEON-стиль (Фаза 1). */ #include #include @@ -14,6 +14,7 @@ #include #include #include "pop_bg.h" +#include "_pop_bg.h" /* контракт с холодной половиной (pop_room.c, банк 7) */ #include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_state.h" @@ -23,34 +24,6 @@ #include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */ #include "pop_guard.h" /* SWORD_2_DRAWN — расширение футпринта под клинок */ -static const char *const ENV_ATL[5] = { - /* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон, - * KID — персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска - * зарастает десятками .atl. */ - "BG\\pop_env0.atl", "BG\\pop_env1.atl", "BG\\pop_env2.atl", - "BG\\pop_env3.atl", "BG\\pop_env4.atl" -}; - -int pop_bg_load(void) __banked -{ - int i; - for (i = 0; i < 5; i++) - if (atlas_load(&pop_env[i], ENV_ATL[i]) != 0) return -1; - if (atlas_load(&pop_wall_at, "BG\\pop_wall.atl") != 0) return -1; - if (atlas_load(&pop_fore_at, "BG\\pop_fore.atl") != 0) return -1; - if (atlas_load(&pop_pot_at, "BG\\pop_pot.atl") != 0) return -1; - return 0; -} - -void pop_bg_free(void) __banked -{ - int i; - for (i = 0; i < 5; i++) - atlas_free(&pop_env[i]); - atlas_free(&pop_wall_at); - atlas_free(&pop_fore_at); - atlas_free(&pop_pot_at); -} /* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а * draw_other_overlay/floor_overlay/полоса потолка). Оверлей включается @@ -85,11 +58,7 @@ void pop_fore_heal(void) __banked /* ---- Константные таблицы (render_room.py) -------------------------- */ static const uint8_t TBL_LINE[3] = {0, 10, 20}; static const uint8_t DOOR_FRAM_SLICE[9] = {67, 59, 58, 57, 56, 55, 54, 53, 52}; -static const uint8_t SPIKES_FRAM_RIGHT[10] = {0, 134, 135, 136, 137, 138, 137, 135, 134, 0}; static const uint8_t SPIKES_FRAM_FORE[10] = {0, 139, 140, 141, 142, 143, 142, 140, 139, 0}; -static const uint8_t BLUELINE_FRAM1[4] = {0, 124, 125, 126}; -static const int8_t BLUELINE_FRAM_Y[4] = {0, -20, -20, 0}; -static const uint8_t BLUELINE_FRAM3[4] = {44, 44, 45, 45}; static const uint8_t LPOS[5] = {58, 41, 37, 20, 16}; static const uint8_t RPOS[4] = {52, 42, 31, 21}; @@ -101,30 +70,6 @@ static const uint8_t RPOS[4] = {52, 42, 31, 21}; #define RES_WALL_MARK_TR 16 #define RES_WALL_MARK_BR 17 -/* ---- Зелье (tiles_10_potion, seg008 draw_tile_anim/draw_tile_fore) ---- - * Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая» - * склянка (id 13) и pot_size=1, остальные — маленькая (id 12). Спрайты — из - * chtab_1 (атлас pop_pot), поэтому НЕ из env: раньше мы рисовали chtab_6 id 12 - * (res212), которого в VDUNGEON нет — оттуда и «мусорный объект». - * Позиции оригинала: склянка xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + - * draw_main_y; пузырёк/маска xh = draw_xh + 3, xl = 1, - * ybottom = draw_main_y − (pot_size << 2) − 14. */ -#define POT_MASK_ID 23 -static const uint8_t POTION_FRAM_BUBB[8] = {0, 16, 17, 18, 19, 20, 21, 22}; - -static uint8_t potion_type(uint8_t modif) { return (uint8_t)((modif & 0xF8) >> 3); } - -static void potion_bubble(int row, int col, uint8_t modif, uint8_t fore) -{ - uint8_t type = potion_type(modif); - int pot_size = (type >= 2 && type < 5) ? 1 : 0; - int x = (POP_COL_XH[col] + 3) * 8 + 1; - int yb = 63 * row + 62 - (pot_size << 2) - 14; - if (type == 0) return; - if (fore) pop_pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ - else pop_pot_b(POT_MASK_ID, x, yb); /* маска (под ним) */ -} - /* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- * * Сид (pop_t_seed) живёт в резиденте: его ставит полная отрисовка комнаты, а * читает кэш раскладки кладки. */ @@ -296,15 +241,6 @@ static void wall_pattern(int row, int col, int which_part) } } -/* topright тайла снизу-слева (seg008.c:410). */ -static void draw_topright(uint8_t tt, int x, int dby) -{ - if (tt == 7 || tt == 12) return; - if (tt == 20) { pop_wall_b(2, x, dby); return; } - if (pop_tile_table[tt].topright_id) - pop_env_b(pop_tile_table[tt].topright_id, x, dby); -} - /* Решётка ворот (portcullis) из ЛЕВОЙ комнаты (seg008.c:1110). */ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) { @@ -330,774 +266,29 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) pop_env_b(DOOR_FRAM_SLICE[gate_frame], x, yb); } -/* ---- Loose floors: кадры дрожащей плиты (seg008 loose_fram_*) ------- * - * Состояние pop_loose_modif[] живёт в pop_map. draw_tile рисует грани - * loose АНИМИРОВАННО по modif (как SDLPoP: сам тайл — лево/низ, ПРАВЫЙ - * СОСЕД — правую грань через draw_tile_anim_right). При modif==0 (покой/ - * статич. запекание) get_loose_frame=0 → базовые кадры 41/42/43. */ -extern uint8_t pop_loose_modif[30]; -extern uint8_t pop_ceil_modif[10]; /* плита-потолок: loose ряда 2 комнаты сверху */ -static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0}; -/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора, - * из которой потом восстанавливает heal). В этом режиме дрожащие кадры - * ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а - * так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера), - * на страницах застынут РАЗНЫЕ кадры — вечное мерцание. Пример: упала плита - * (0,6); её запечка перерисовывает и соседа, а правый угол СОСЕДНЕЙ плиты - * (0,5), который рисуется в ячейке (0,6), был в этот момент в тряске. - * Фон обязан быть состоянием ПОКОЯ — анимация каждый кадр кладётся поверх - * (банк SPRITE, только видео-ОЗУ). */ -/* draw_leveldoor (seg008:1D29) — дверь уровня. Рисуется при обработке - * ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8px (xh+1): - * 99 низ лестницы (всегда), - * 144 марш лестницы за створкой (когда створка приподнята, modif != 0), - * 33 слайс створки — повторяется вниз с шагом 4px, пока не дойдёт до - * y = ybottom − modif: modif 0 = закрыто (створка на всю высоту), - * 43 = открыто (остаётся только верхняя кромка), - * 34 верх коробки двери. - * Куски непрозрачные (blitters_0_no_transp) — как в оригинале. */ -static void draw_leveldoor(uint8_t modif, int xh, int dmy) + +/* ---- Точки входа для ХОЛОДНОЙ половины (pop_room.c, банк 7) ---------- * + * Тонкие обёртки, а не пометка самих тел `__banked`: трамплин выбирается + * ОБЪЯВЛЕНИЕМ, поэтому пометка тела заставила бы платить 654 такта и + * ГОРЯЧИХ вызывающих — fore-проход зовёт wall_pattern до девяти раз за кадр. + * С обёрткой горячий путь идёт прямым call, а трамплин достаётся только + * холодному (полная отрисовка комнаты: ~40 вызовов, 26 000 тактов РАЗОВО на + * вход в комнату). См. _pop_bg.h и memory/sdcc_banked_call_rules. */ +void pop_wall_pattern_b(int row, int col, int which_part) __banked { - int x = (xh + 1) * 8; - int ybottom = dmy - 13; - int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */ - int y = ybottom - (int)modif; - /* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО - * тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из - * draw_tile_anim_right, разбирая левого соседа). +8 у palace-уровней - * (tbl_level_type) — на уровне 1 не применяется. */ - pop_leveldoor_right = xh * 8 + 48; - pop_env_b(99, x, ybottom); - if (modif) - pop_env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */ - for (;;) { - pop_env_b(33, x, ylo); - if (y > ylo) ylo += 4; - else break; - } - /* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое - * значение (низ поднятой створки), а не стартовое. */ - pop_leveldoor_ybottom = ylo; - pop_env_b(34, x, dmy - 64); + wall_pattern(row, col, which_part); } -/* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */ -static void draw_tile(int row, int col) +void pop_wall_pattern_reset_b(void) __banked { - uint8_t code = pop_tile_code(row, col); - uint8_t lcode = pop_tile_code(row, col - 1); - uint8_t lmod = pop_tile_mod(row, col - 1); - /* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener - * (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05). - * Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого - * соседа (его правая грань). */ - if (code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 1; } - else if (code == 0x06) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 5; } - if (lcode == 0x0F) { if ((pop_doorlink2(lmod) & 0x1F) > 1) { lcode = 1; lmod = 0; } } - else if (lcode == 0x06) { if ((pop_doorlink2(lmod) & 0x1F) > 1) lcode = 5; } - int xh = POP_COL_XH[col]; - int x = xh * 8; - int dby = 63 * row + 65; - int dmy = dby - 3; - const piece *t = &pop_tile_table[code]; - const piece *lt = &pop_tile_table[lcode]; - uint8_t rbl_code; - uint8_t wmod; - - /* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0 - * комнаты снизу (load_rowbelow, seg008:368): row_below_left_[col] = - * room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */ - if (row + 1 > 2) { - int lc = col - 1; - /* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате - * (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда; - * «стена» там стоит, только если такой комнаты нет — как в - * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ - if (lc < 0) rbl_code = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20; - else rbl_code = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 1; - } else rbl_code = pop_tile_code(row + 1, col - 1); - - /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */ - if (code == 0 || code == 9 || code == 12 || code == 26) { - draw_topright(rbl_code, x, dby); - /* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op - * (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет - * floor_right: lt->floor_right. */ - } - - /* --- draw_tile_right (seg008.c:456) --- */ - if (code != 20) { - if (lcode == 20) { - pop_wall_b(1, x, pop_tile_table[20].right_y + dmy); /* грань стены */ - } else if (lcode == 0) { - if (lmod <= 3 && BLUELINE_FRAM1[lmod]) - pop_env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy); - } else if (lcode == 1) { - uint8_t num; - pop_env_b(42, x, pop_tile_table[1].right_y + dmy); /* правый треуг. пола */ - num = (lmod <= 3) ? lmod : 0; - if (num != 0) /* dungeon: level_type=0 */ - pop_env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */ - } else { - if (lt->right_id) - pop_env_b(lt->right_id, x, lt->right_y + dmy); - /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */ - if (lcode == 19 || lcode == 30) - pop_env_b(146, x, dby - 28); - /* stripe — только палас, dungeon пропускает */ - } - } - - /* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для - * верхней полосы (row<0, draw_tile_aboveroom) НЕ рисуется + защита от - * отрицательного индекса в pop_loose_modif. --- */ - if (row >= 0) { - if (lcode == 4) { /* gate: решётка */ - draw_gate_back(lmod, xh, dby, dmy); - } else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ - pop_env_b(LOOSE_FRAM_RIGHT[pop_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); - } else if (lcode == 2) { /* spike: статич. кадр */ - uint8_t sf = (lmod & 0x80) ? 5 : lmod; - if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) - pop_env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); - } else if (lcode == 16) { /* дверь уровня */ - draw_leveldoor(lmod, xh, dmy); - } - /* Пламя факела (seg008:560, chtab_1 id modifier+1) в фон НЕ запекаем: - * оно анимируется каждый кадр, а heal вернул бы запечённый кадр из-под - * нового. Рисует pop_torch_draw (динамика, pop_process_trobs). */ - } - - /* --- draw_tile_bottom (seg008.c:570) --- */ - if (code == 20) { - wmod = pop_wall_modifier(row, col); - pop_wall_b(POP_WALL_FRAM_BOTTOM[wmod], x, dby); /* сплошная нижняя грань */ - wall_pattern(row, col, 0); - } else if (t->bottom_id) { - pop_env_b(t->bottom_id, x, dby); - } - - /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В - * верхней полосе (row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху): - * её состояние живёт отдельно, в pop_ceil_modif[col] (pop_map). --- */ - if (code == 11) - pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col] - : pop_ceil_modif[col])], x, dby); - - /* --- draw_tile_base (seg008.c:611). Верхняя полоса (row<0) базу НЕ - * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */ - if (row >= 0) { - uint8_t base_id = t->base_id; - if (code == 11) /* loose: левая грань (анимир.) */ - base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])]; - else if (code == 0x0F && lcode == 0) /* opener, слева пусто */ - base_id = 148; /* левая половина без пола */ - if (base_id) - pop_env_b(base_id, x, t->base_y + dmy); - } - - /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в - * СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики - * рисуется только правым соседом со сдвигом → шахта над самой пикой - * пустая. */ - if (row >= 0 && code == 2) { - uint8_t sm = pop_tile_mod(row, col); - uint8_t sf = (sm & 0x80) ? 5 : sm; - if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf]) - pop_env_b(POP_SPIKES_FRAM_LEFT[sf], x, dmy - 2); - } - /* Меч-предмет на полу (тот же draw_tile_anim, case tiles_22_sword): - * chtab_1 (наш pot-атлас), id = (modif==1)+10, низ на draw_main_y-3. - * В оригинале это midtable (рисуется вместе с объектами) — у нас статикой - * в фоне: пока меч лежит, он не анимируется, а Kid и так поверх фона. */ - if (row >= 0 && code == 22) - pop_pot_b((uint8_t)((pop_tile_mod(row, col) == 1) + 10), x, dmy - 3); - - /* --- draw_tile_fore (seg008.c:690) --- */ - if (code == 20) { - wmod = pop_wall_modifier(row, col); - pop_wall_b(POP_WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ - wall_pattern(row, col, 1); - } - if (code == 10) { /* зелье: chtab_1, не env fore_id. - * Пузырёк — динамика (pop_potion_draw). */ - pop_potion_flask(row, col, pop_tile_mod(row, col)); - } else if (t->fore_id) { - pop_fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); - } + wall_pattern_reset(); } -/* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- * - * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать - * ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif - * внутри draw_tile). */ -void pop_loose_shake_draw(int row, int col) __banked +void pop_gate_back_b(uint8_t modl, int xh, int dby, int dmy) __banked { - int x = POP_COL_XH[col] * 8; - /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх — - * перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из - * pop_loose_modif). Порт redraw_tile_height (loose + правый сосед). - * NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое - * решение (bake тайла как empty + рисовать loose всегда динамически) - * отложено; wipe-в-чёрный делал плиту невидимой — не годится. */ - pop_heal_off(x, 63 * row + 46, 64, 24); - gfx_set_bank(GFX_BANK_SPRITE); - draw_tile(row, col); /* loose: лево+низ (анимир.) */ - if (col + 1 < 10) - draw_tile(row, col + 1); /* сосед: правая грань loose (анимир.) + его пол */ - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif - * (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2 - * читает lmod = pop_t_bg[row*10+col] = живой modif пики). pop_t_bg должен указывать - * на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile. - * heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */ -void pop_spike_redraw(int row, int col) __banked -{ - int x = POP_COL_XH[col] * 8; - /* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ - * redraw ДВУХ тайлов — своего и правого соседа (остриё рисуется в его - * ячейке). Именно ПОЛНЫЙ: рисовать только два спрайта остриёв нельзя — - * порядок слоёв внутри тайла (draw_tile_anim_right идёт ПЕРВЫМ, база и - * fore соседа ложатся ПОВЕРХ) тогда теряется, и остриё лезет на колонну - * справа. Заодно draw_tile сам восстанавливает вклад соседних пик: - * в ячейке col — правое остриё пики col-1 (ветка lcode==2). - * Шипы выдвигаются выше линии пола (dmy-7), поэтому heal с запасом вверх - * (63*row+34 .. +74); ширина 64 = обе ячейки. */ - pop_heal_off(x, 63 * row + 34, 64, 40); - gfx_set_bank(GFX_BANK_SPRITE); - draw_tile(row, col); - if (col + 1 < 10) - draw_tile(row, col + 1); - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* Перерисовать ЛЕВЫЙ шов (col0) на back-странице по живому modif соседа — - * ворота соседней комнаты слева (portcullis room8) видны в col0 текущей - * комнаты через draw_gate_back (draw_tile ветка lcode==4, lmod = pop_t_lbg[0]). - * pop_t_lbg (lcol_bg) должен указывать на живой modif (roomtest обновляет из - * room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake): - * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары - * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ -void pop_room_redraw_seam_left(void) __banked -{ - gfx_set_bank(GFX_BANK_NORMAL); - setfillstyle(SOLID_FILL, BLACK); - /* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2, - * а бары ворот начинаются с y=3 (gate_top_y = dby−62). Раньше bar шёл от - * POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать ещё и - * draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации решётки. */ - bar(0, POP_YOFF + 3, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ - gfx_set_bank(GFX_BANK_TRANSPARENT); - draw_tile(0, 0); /* фон col0 + бары ворот (живой modif) */ - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- * - * В полосе кладки у потолка (наш ряд −1) виден только НИЗ плиты - * (draw_tile_aboveroom: bottom+loose, всё выше отрезано клипом POP_YOFF). - * Дрожащий кадр берётся из pop_ceil_modif[col] (см. draw_loose выше). - * shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту - * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */ -#define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */ - -void pop_ceil_shake_draw(int col) __banked -{ - int x = POP_COL_XH[col] * 8; - pop_heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ - gfx_set_bank(GFX_BANK_SPRITE); - pop_t_clip_top = POP_YOFF; - draw_tile(-1, col); - if (col + 1 < 10) draw_tile(-1, col + 1); - pop_t_clip_top = 0; - gfx_set_bank(GFX_BANK_NORMAL); -} - -void pop_ceil_bake_empty(int col) __banked -{ - int x = POP_COL_XH[col] * 8; - gfx_set_bank(GFX_BANK_NORMAL); - setfillstyle(SOLID_FILL, BLACK); - bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H); /* стереть плиту в ОЗУ-копии */ - gfx_set_bank(GFX_BANK_TRANSPARENT); - pop_t_clip_top = POP_YOFF; - pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ - /* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только - * два тайла ряда −1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей - * нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут: - * • ряд −1, колонки col−1..col+1 — сама полоса кладки у потолка; - * • ряд 0, колонки col−1..col+1 — их верхушки заходят в те же 8 px. - * Раньше перерисовывались только (−1,col) и (−1,col+1), и в полосе - * оставался чёрный прямоугольник ~13×3 px (замер: экранные y 47..52 при - * полосе 44..52), который heal возвращал каждый кадр. Признак, что дело - * именно в запечке: после выхода из комнаты и возврата бара нет. */ - { - int c0 = (col > 0) ? col - 1 : 0; - int c1 = (col + 1 < 9) ? col + 1 : 9; - int c; - for (c = c0; c <= c1; c++) draw_tile(-1, c); - for (c = c0; c <= c1; c++) draw_tile(0, c); - } - pop_t_bake_rest = 0; - pop_t_clip_top = 0; - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* ---- Анимация факела и зелья (динамика, каждый кадр) ---------------- * - * Оба кадра рисуются ПОВЕРХ статического фона и стираются heal'ом своей - * области (фон запечён БЕЗ них). Факел: пламя chtab_1 id modifier+1 в - * ячейке ПРАВОГО соседа (seg008:560: xh = draw_xh + 1, ybottom = - * draw_main_y − 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька - * potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y − - * (pot_size<<2) − 14 (seg008:668). */ -void pop_torch_draw(int row, int col, uint8_t modif) __banked -{ - /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = POP_COL_XH[col+1] + 1 - * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env - * 146), которое тоже лежит в ячейке соседа. */ - int x = (col + 1 < 10 ? POP_COL_XH[col + 1] : POP_COL_XH[col] + 4) * 8 + 8; - int yb = 63 * row + 62 - 40; - if (modif >= 9) return; - /* heal НЕ нужен: оригинал рисует пламя blitters_0_no_transp (seg008 - * draw_tile_anim_right), и все 9 кадров лежат на общем канвасе 16x18 — - * непрозрачный блит сам полностью накрывает предыдущий кадр. Кадры - * упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS). */ - gfx_set_bank(GFX_BANK_SPRITE); - pop_pot_b((uint8_t)(modif + 1), x, yb); - gfx_set_bank(GFX_BANK_NORMAL); -} - -void pop_potion_draw(int row, int col, uint8_t modif) __banked -{ - uint8_t type = potion_type(modif); - int pot_size = (type >= 2 && type < 5) ? 1 : 0; - int x = (POP_COL_XH[col] + 3) * 8 + 1; - int yb = 63 * row + 62 - (pot_size << 2) - 14; - if (type == 0) return; - pop_heal_off(x - 1, yb - 10, 8, 12); - gfx_set_bank(GFX_BANK_SPRITE); - pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ - pop_pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая - * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + - * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе - * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ -void pop_floor_bake(int row, int col) __banked -{ - int x = POP_COL_XH[col] * 8; - int yb = POP_YOFF + 63 * row; - /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. - * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже - * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */ - gfx_set_bank(GFX_BANK_NORMAL); - setfillstyle(SOLID_FILL, BLACK); - bar(x, yb + 26, x + 59, yb + 64); - gfx_set_bank(GFX_BANK_TRANSPARENT); - pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ - draw_tile(row, col); - if (col + 1 < 10) draw_tile(row, col + 1); - pop_t_bake_rest = 0; - gfx_set_bank(GFX_BANK_NORMAL); -} - -void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); } - -/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка, - * см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет, - * а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое. - * Куски двери непрозрачные — wipe не нужен. */ -void pop_leveldoor_redraw(int row, int col) __banked -{ - gfx_set_bank(GFX_BANK_TRANSPARENT); - draw_tile(row, col); - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob, - * seg007:01E6 — им заканчивается animate_door). (row,col) — сами ворота; - * решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому - * wipe и redraw идут по ней. Логика ровно как у левого шва - * (pop_room_redraw_seam_left), только колонка произвольная. - * - * Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при - * ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары. Полоса — ровно - * зона решётки: от gate_top_y (dby−62 = 63*row+3) до низа (63*row+64); - * выше начинается ряд −1, ниже — стена ряда ниже. - * - * Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их - * не видно — оригинал такой trob тоже пропускает - * (get_trob_right_pos_in_drawn_room → 30). Для взгляда из комнаты справа - * работает pop_room_redraw_seam_left. */ -void pop_gate_redraw(int row, int col) __banked -{ - int x, yt; - if (col + 1 > 9 || row < 0 || row > 2) return; - x = POP_COL_XH[col + 1] * 8; - yt = POP_YOFF + 63 * row; - gfx_set_bank(GFX_BANK_NORMAL); - setfillstyle(SOLID_FILL, BLACK); - bar(x, yt + 3, x + 31, yt + 64); - gfx_set_bank(GFX_BANK_TRANSPARENT); - draw_tile(row, col + 1); - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт - * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И - * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — - * рисовал loose-правый-край pop_env_b(42); теперь пусто → чистая грань). - * pop_t_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые - * пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */ -void pop_loose_bake_empty(int row, int col) __banked -{ - int x = POP_COL_XH[col] * 8; - int xr = POP_COL_XH[col + 1] * 8; - int yb = 63 * row + 46; - /* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора): - * стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе - * heal вернул бы старый пол из ОЗУ-копии. Ширина 40 — кусок пола - * заходит на x+32..+40. */ - /* НИЗ строго 63*row+65 = низ собственной графики плиты (bottom_id 43/73/74 - * стоит ровно на dby; base 41/69/70 — до dmy=62; правая грань 42/66/67 — - * до dby-1). Ниже начинается СТЕНА ряда ниже (высокий спрайт), которую - * здесь не перерисовываем: заливка на 9 строк глубже оставляла ЧЁРНЫЙ БОКС - * под соседним столбом, причём в ОЗУ-копии — то есть heal возвращал его - * снова, и на второй странице дабл-буфера его не было → мерцание. - * Тот же урок уже учтён в pop_floor_bake. */ - yb += POP_YOFF; - gfx_set_bank(GFX_BANK_NORMAL); - setfillstyle(SOLID_FILL, BLACK); - bar(x, yb, x + 39, POP_YOFF + 63 * row + 65); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */ - if (col + 1 < 10) - bar(xr, yb, xr + 31, POP_YOFF + 63 * row + 65); /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */ - /* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты - * снизу) + правый сосед (его левая грань: был loose-край pop_env_b(42), - * теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в - * ОЗУ-копию → heal сохранит. */ - gfx_set_bank(GFX_BANK_TRANSPARENT); - pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ - draw_tile(row, col); - if (col + 1 < 10) - draw_tile(row, col + 1); - pop_t_bake_rest = 0; - gfx_set_bank(GFX_BANK_NORMAL); -} - -/* ---- Падающий кусок loose (mob, seg007 move_loose) ------------------ * - * Упрощ. одиночный кусок (в комнате 1 — один loose). Модель fixed- - * corridor: коридор падения (колонка тайла) heal'ится каждый кадр (стирает - * прошлый кусок, возвращает печёный фон — empty+«стена вниз»), поверх — - * кадр 0 loose (41/42/43) на текущей y. Гравитация: speed 0→+3/кадр - * (cap 29), y+=speed. Двоебуфер: коридор фиксирован → heal чистит любой - * прошлый кадр ЭТОЙ страницы. Ниже поля кусок прячет clip_borders. */ -/* Падающие куски — ТАБЛИЦА (порт mobs[14]/mobs_count, seg007:965). Одного - * куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе - * подряд, и второй спавн затирал состояние первого — его пиксели оставались - * висеть в воздухе (на каждой странице на своей y → мерцание), а щебень - * появлялся только под одной плитой. 4 слота: больше одновременно падающих - * плит в одной комнате практически не бывает (в оригинале 14). */ -#define MOB_MAX 4 -typedef struct { - uint8_t active; /* 0 нет; 1 падает */ - uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */ - uint8_t defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */ - int x; /* левый край (POP_COL_XH*8) */ - int y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */ - int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ - int8_t speed; - int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ - uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */ -} mob_t; -static mob_t mobs[MOB_MAX]; - -/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на - * который он лёг → приложение/pop_map ставит debris + do_knock. */ -/* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а - * писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */ - -/* y_loose_land / y_something (seg007): [row+1], row = −1..3. Ряд −1 — - * плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный - * tilepos, у нас — свой ряд полосы у потолка. */ -static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254}; -static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25}; -#define MOB_Y_NONE 0x7FFF - -void pop_loose_mob_spawn(int row, int col) __banked -{ - uint8_t i; - mob_t *m = 0; - /* свободный слот = не летит И дочистка завершена (иначе затрём хвост, - * который ещё не стёрт со второй страницы) */ - for (i = 0; i < MOB_MAX; i++) - if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; } - if (!m) return; /* таблица полна — кусок без спрайта (тайл всё равно пуст) */ - m->x = POP_COL_XH[col] * 8; - m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */ - m->row = (int8_t)row; - m->col = (int8_t)col; - m->room = pop_t_room; /* curmob.room (add_mob, seg007:1010) */ - m->speed = 0; - m->active = 1; - m->clean = 0; - m->defer = 0; - m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; -} - -/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26) - * с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46: - * ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину - * [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной - * странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */ -#define MOB_X0(mx) ((mx) - 4) -#define MOB_W 64 /* mob_x-4 .. mob_x+59 (крышка +57 с запасом 2px) */ - -/* Остановить падающий кусок (при смене комнаты — состояние per-tile не - * должно утечь в новую комнату). */ -/* Тайл ОБЪЕКТА Kid (set_objtile_at_char, seg006:1833) — в момент обработки - * этого тайла спрайт Kid попадает в midtable. Задаётся в pop_fore_over_char, - * читается other_overlay_tile (порядок «кто поверх кого»). */ -static int8_t obj_col, obj_row; - -static void mob_render(mob_t *m, uint8_t pg); - -void pop_loose_mob_reset(void) __banked -{ - uint8_t i; - for (i = 0; i < MOB_MAX; i++) { - mobs[i].active = 0; mobs[i].clean = 0; mobs[i].defer = 0; - mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; - } -} - -/* Смена комнаты. Куски НЕ останавливаем: в оригинале mobs[] чистится только - * в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо - * от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид - * не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12 - * плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только - * heal-историю: новая комната перерисована целиком, старых пикселей нет. */ -void pop_loose_mob_room_changed(void) __banked -{ - uint8_t i; - for (i = 0; i < MOB_MAX; i++) { - mobs[i].defer = 0; - mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; - /* дочистка хвоста относилась к покинутой комнате — она уже не нужна */ - if (!mobs[i].active) mobs[i].clean = 0; - } -} - -/* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки - * и соседей), в чужой — прямо из данных уровня. */ -static uint8_t mob_tile_at(mob_t *m) -{ - if (m->room == pop_t_room) return (uint8_t)(pop_tile_code(m->row, m->col) & 0x1F); - if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1; /* кромка = пол */ - return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F); -} - -static void mob_tick_one(mob_t *m, uint8_t pg) -{ - uint8_t here; - if (!m->active && !m->clean) return; - here = (uint8_t)(m->room == pop_t_room); - /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой - * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; - * плита-потолок падает через весь ряд 0, поэтому чистим по фактической - * прошлой позиции. */ - if (here && m->prev_y[pg] != MOB_Y_NONE) { - pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); - m->prev_y[pg] = MOB_Y_NONE; - } - if (!m->active) { m->clean--; return; } - /* гравитация (move_loose): ускорение до 29 */ - if (m->speed < 29) m->speed += 3; - m->y += m->speed; - /* move_loose (seg007:1126): низ куска пересёк границу своего ряда — - * смотрим тайл этого ряда. Пусто/loose — валится дальше (порт - * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); - * пол — приземление (loose_land: debris + do_knock). */ - if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { - uint8_t t = mob_tile_at(m); - if (t == 0 || t == 11) { - m->row++; /* mob_down_a_row */ - } else if (!here) { - /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, сигнальная - * пара pop_loose_landed/pop_debris_at обслуживает только текущую - * комнату — щебень кладём прямо в данные уровня (loose_land пишет - * в curr_room_tiles комнаты curmob.room, а не отрисованной). */ - pop_level_set_tile(m->room, (uint8_t)(m->row * 10 + m->col), 0x0E); - m->active = 0; m->clean = 0; - return; - } else if (pop_loose_landed) { - /* сигнал прошлого приземления ещё не разобран (один за кадр) — - * подержим кусок на границе и приземлим следующим кадром */ - m->y = MOB_Y_BOUND[m->row + 1]; - } else { - m->y = MOB_Y_BOUND[m->row + 1]; - pop_loose_landed = (uint8_t)(m->row * 10 + m->col + 1); - m->active = 0; m->clean = 2; /* кусок исчез — на его месте debris */ - return; - } - } - if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */ - m->active = 0; m->clean = (uint8_t)(here ? 2 : 0); - return; - } - if (!here) return; /* чужая комната — считаем, но не рисуем */ - /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ - * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри - * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто - * ниже — тот позже, т.е. поверх). Тайл падающего куска — draw_mob - * (seg007:13E5): row = y_to_row_mod4(y), col = xh>>2. Значит кусок - * поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при - * совпадении тайлов — если он ниже по y. */ - { - int8_t mrow = (int8_t)(((m->y + 60) / 63) % 4 - 1); /* y_to_row_mod4 */ - uint8_t over; - if (mrow != obj_row) over = (mrow < obj_row); /* ряды идут 2,1,0 */ - else if (m->col != obj_col) over = (m->col > obj_col); - else over = (m->y > pop_cd[POP_CH_KID].fpy); - if (over) { m->defer = 1; return; } - } - mob_render(m, pg); -} - -void pop_loose_mob_tick(void) __banked -{ - uint8_t pg = gfx_get_draw_page() & 1, i; - for (i = 0; i < MOB_MAX; i++) - mob_tick_one(&mobs[i], pg); -} - -static void mob_render(mob_t *m, uint8_t pg) -{ - /* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска — - * left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1, - * заходит в тайл СПРАВА) на obj_y-1. */ - gfx_set_bank(GFX_BANK_SPRITE); - pop_env_b(43, m->x, m->y); - pop_env_b(41, m->x, m->y - 3); - pop_env_b(42, m->x + 32, m->y - 1); - m->prev_y[pg] = m->y; - /* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob: - * set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob). - * Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола, - * её правый край ПРЯЧЕТСЯ за передней гранью пола. Перерисовываем - * сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу - * до heal'а следующего кадра — виден как хвост во время падения). */ - if (m->col + 1 < 10) { - /* Ряд соседа берём ИЗ КООРДИНАТЫ куска (y_to_row_mod4(ypos)), а НЕ из - * m->row: последний — логический счётчик «сквозь какой ряд летим», и - * mob_down_a_row уводит его на ряд ВПЕРЁД (для плиты нижнего ряда — - * сразу в 3, которого нет). Тогда пол соседа не перерисовывался - * вообще, и правый кусок 42 оставался ПОВЕРХ него: грань плиты видна - * через пол + перекрыта передняя грань пола. - * Плюс ВТОРОЙ тайл — под ВЕРХОМ куска (ypos-18), пока кусок висит на - * границе рядов. Порт draw_mob (seg007:13E5). */ - int8_t r = (int8_t)(((m->y + 60) / 63) % 4 - 1); - int8_t rt = (int8_t)(((m->y - 18 + 60) / 63) % 4 - 1); - if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */ - draw_tile(r, m->col + 1); - pop_t_clip_top = 0; - if (rt != r) { - if (rt < 0) pop_t_clip_top = POP_YOFF; - draw_tile(rt, m->col + 1); - pop_t_clip_top = 0; - } - } - gfx_set_bank(GFX_BANK_NORMAL); - /* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */ - pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30); -} - -/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать - * после отрисовки персонажей, до fore-слоя. */ -void pop_loose_mob_draw_over(void) __banked -{ - uint8_t pg = gfx_get_draw_page() & 1, i; - for (i = 0; i < MOB_MAX; i++) - if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); } -} - -/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и - * y (низ куска). 0 — куска нет. */ -uint8_t pop_loose_mob_pos(int *col, int *y) __banked -{ - uint8_t i; - /* check_loose_fall_on_kid (seg007:1192) начинается с Char.room == - * curmob.room: кусок, падающий в покинутой комнате, Киду по голове не - * бьёт. */ - for (i = 0; i < MOB_MAX; i++) - if (mobs[i].active && mobs[i].room == pop_t_room) { - *col = mobs[i].col; *y = mobs[i].y; return 1; - } - return 0; -} - -/* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл- - * буфера). Узкий per-спрайт heal НЕ используем: accel-выравнивание блока - * оставляет 1–2px слайверы правой грани → на одной странице мерцает. - * Вместо этого борта чистит pop_room_clip_borders (полоса, но ТОЛЬКО в - * кадрах падения — гейт по флагу). */ -static uint8_t border_dirty; - -void pop_clip_sprite(int x, int w, int sy_top, int h) __banked -{ - int sy_bot = sy_top + h; - (void)x; (void)w; - if (sy_bot > POP_CLIP_BOTTOM || sy_top < POP_YOFF) - border_dirty = 2; /* 2 кадра = обе страницы */ -} - -/* Клип бортов (см. pop_bg.h): работает ТОЛЬКО пока border_dirty (кто-то - * вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает - * чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу - * + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */ -void pop_room_clip_borders(void) __banked -{ - if (!border_dirty) return; - border_dirty--; - /* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320 - * не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска - * по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах - * падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там - * тонет в цене самих 320×28 пикселей. Не «оптимизировать» повторно. */ - gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */ - gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */ -} - -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) __banked -{ - int row, col; - pop_t_room = room_num; - wall_pattern_reset(); /* кэш кладки привязан к номеру комнаты */ - pop_t_fg = fg; pop_t_bg = bg; - pop_t_lfg = lcol_fg; pop_t_lbg = lcol_bg; - - /* прозрачный блит: 0xFF не пишется (чёрный фон/нижние слои проступают) */ - gfx_set_bank(GFX_BANK_TRANSPARENT); - pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ - for (row = 2; row >= 0; row--) /* снизу вверх (2,1,0) */ - for (col = 0; col < 10; col++) - draw_tile(row, col); - /* Полоса кладки у потолка (seg008 draw_room: доп. ряд из room_A, row2 - * при draw_main_y=-1): нижние/правые грани ряда 2 комнаты сверху. Кадры - * высокие — верх ушёл бы в борт (за поле); клип по верхней границе поля - * (POP_YOFF) рисует ТОЛЬКО срез у потолка, без зачернения борта. */ - pop_t_clip_top = POP_YOFF; - for (col = 0; col < 10; col++) - draw_tile(-1, col); - pop_t_clip_top = 0; - pop_t_bake_rest = 0; - gfx_set_bank(GFX_BANK_NORMAL); + draw_gate_back(modl, xh, dby, dmy); } /* ---- fore-over-Kid (порт seg003 redraw_at_char + seg008 set_char_collision) -- @@ -1206,6 +397,9 @@ static void fore_tile(int row, int col) /* передний слой одно fore_only_tile(row, col); } +/* Определение общих с pop_room.c obj-координат (объявление — _pop_bg.h). */ +int8_t pop_bg_obj_col, pop_bg_obj_row; + /* Пол-оверлей при подъёме (draw_floor_overlay, seg008:1457): когда Kid * подтягивается на floor-подобный тайл назначения (floor/pillar/stuck/torch), * а тайл СЛЕВА пуст (т.е. кромка), передний край пола перекрывает нижнюю @@ -1322,7 +516,7 @@ static void other_overlay_tile(int row, int col) * спрайтом его перерисовывать не нужно. */ if (code == 0) return; /* Тайл идёт в обходе РАНЬШЕ тайла Kid → его оверлей лёг бы ПОД спрайт. */ - if (row > obj_row || (row == obj_row && col <= obj_col)) return; + if (row > pop_bg_obj_row || (row == pop_bg_obj_row && col <= pop_bg_obj_col)) return; left = pop_tile_code(row, col - 1); if (left == 0) { overlay_mid_tile(row, col); return; } /* ветка 1: сосед слева пуст */ if (col > 0 && pop_tile_code(row, col - 2) == 0) { @@ -1331,7 +525,7 @@ static void other_overlay_tile(int row, int col) * tilepos рисуются СРАЗУ ПОСЛЕ оверлея — Kid возвращается наверх. * Без этого правая грань соседнего столба (кадр 93) остаётся поверх * прыгающего Kid (баг «тень на дальнем столбе поверх Кида»). */ - if (row == obj_row && (col == obj_col + 1)) return; + if (row == pop_bg_obj_row && (col == pop_bg_obj_col + 1)) return; overlay_mid_tile(row, col); } } @@ -1442,12 +636,12 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y, * попадает в midtable — база порядка окклюзии в other_overlay_tile. * run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/ * подъём — колонка на одну ЛЕВЕЕ curr_col. */ - obj_row = ch->curr_row; - obj_col = ch->curr_col; - if (action == 1) { obj_row = rB; obj_col = cL; } + pop_bg_obj_row = ch->curr_row; + pop_bg_obj_col = ch->curr_col; + if (action == 1) { pop_bg_obj_row = rB; pop_bg_obj_col = cL; } if ((frame >= 135 && frame < 149) || action == 2 || action == 3 || action == 4 || action == 6) - obj_col--; + pop_bg_obj_col--; gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, heal восстановит */ /* Шовная окклюзия ворот (порт redraw_at_char для шва): футпринт зашёл за * ЛЕВЫЙ шов (cLraw<0) и у соседа слева в этом ряду решётка ворот (код 4) — diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c new file mode 100644 index 0000000..b9085e6 --- /dev/null +++ b/applications/PoP/roomtest/pop_room.c @@ -0,0 +1,846 @@ +/* + * pop_room.c — ХОЛОДНАЯ половина слоя фона PoP: полная отрисовка комнаты + * (порт seg008.c draw_tile / render_room.py), точечные перерисовки тайлов + * при событиях (пики, ворота, кнопки, дверь уровня, дрожащие плиты) и + * падающие куски loose (mob, seg007 move_loose). Всё это работает раз на + * комнату или раз на событие, поэтому живёт в ОТДЕЛЬНОМ банке и платит + * трамплин за вызовы горячей половины — см. _pop_bg.h, там же граница реза. + * + * Только DUNGEON-стиль (Фаза 1). + */ +#include +#include +#include +#include +#include +#include "pop_bg.h" +#include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */ +#include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */ +#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ +#include "pop_state.h" +#include "pop_geom.h" +#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ +#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */ +#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */ + +static const char *const ENV_ATL[5] = { + /* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон, + * KID — персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска + * зарастает десятками .atl. */ + "BG\\pop_env0.atl", "BG\\pop_env1.atl", "BG\\pop_env2.atl", + "BG\\pop_env3.atl", "BG\\pop_env4.atl" +}; + +int pop_bg_load(void) __banked +{ + int i; + for (i = 0; i < 5; i++) + if (atlas_load(&pop_env[i], ENV_ATL[i]) != 0) return -1; + if (atlas_load(&pop_wall_at, "BG\\pop_wall.atl") != 0) return -1; + if (atlas_load(&pop_fore_at, "BG\\pop_fore.atl") != 0) return -1; + if (atlas_load(&pop_pot_at, "BG\\pop_pot.atl") != 0) return -1; + return 0; +} + +void pop_bg_free(void) __banked +{ + int i; + for (i = 0; i < 5; i++) + atlas_free(&pop_env[i]); + atlas_free(&pop_wall_at); + atlas_free(&pop_fore_at); + atlas_free(&pop_pot_at); +} + +static const uint8_t SPIKES_FRAM_RIGHT[10] = {0, 134, 135, 136, 137, 138, 137, 135, 134, 0}; + +static const uint8_t BLUELINE_FRAM1[4] = {0, 124, 125, 126}; +static const int8_t BLUELINE_FRAM_Y[4] = {0, -20, -20, 0}; +static const uint8_t BLUELINE_FRAM3[4] = {44, 44, 45, 45}; + +/* ---- Зелье (tiles_10_potion, seg008 draw_tile_anim/draw_tile_fore) ---- + * Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая» + * склянка (id 13) и pot_size=1, остальные — маленькая (id 12). Спрайты — из + * chtab_1 (атлас pop_pot), поэтому НЕ из env: раньше мы рисовали chtab_6 id 12 + * (res212), которого в VDUNGEON нет — оттуда и «мусорный объект». + * Позиции оригинала: склянка xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + + * draw_main_y; пузырёк/маска xh = draw_xh + 3, xl = 1, + * ybottom = draw_main_y − (pot_size << 2) − 14. */ +#define POT_MASK_ID 23 +static const uint8_t POTION_FRAM_BUBB[8] = {0, 16, 17, 18, 19, 20, 21, 22}; + +static uint8_t potion_type(uint8_t modif) { return (uint8_t)((modif & 0xF8) >> 3); } + + + +/* topright тайла снизу-слева (seg008.c:410). */ +static void draw_topright(uint8_t tt, int x, int dby) +{ + if (tt == 7 || tt == 12) return; + if (tt == 20) { pop_wall_b(2, x, dby); return; } + if (pop_tile_table[tt].topright_id) + pop_env_b(pop_tile_table[tt].topright_id, x, dby); +} + + +/* ---- Loose floors: кадры дрожащей плиты (seg008 loose_fram_*) ------- * + * Состояние pop_loose_modif[] живёт в pop_map. draw_tile рисует грани + * loose АНИМИРОВАННО по modif (как SDLPoP: сам тайл — лево/низ, ПРАВЫЙ + * СОСЕД — правую грань через draw_tile_anim_right). При modif==0 (покой/ + * статич. запекание) get_loose_frame=0 → базовые кадры 41/42/43. */ +static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0}; + +/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора, + * из которой потом восстанавливает heal). В этом режиме дрожащие кадры + * ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а + * так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера), + * на страницах застынут РАЗНЫЕ кадры — вечное мерцание. Пример: упала плита + * (0,6); её запечка перерисовывает и соседа, а правый угол СОСЕДНЕЙ плиты + * (0,5), который рисуется в ячейке (0,6), был в этот момент в тряске. + * Фон обязан быть состоянием ПОКОЯ — анимация каждый кадр кладётся поверх + * (банк SPRITE, только видео-ОЗУ). */ +/* draw_leveldoor (seg008:1D29) — дверь уровня. Рисуется при обработке + * ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8px (xh+1): + * 99 низ лестницы (всегда), + * 144 марш лестницы за створкой (когда створка приподнята, modif != 0), + * 33 слайс створки — повторяется вниз с шагом 4px, пока не дойдёт до + * y = ybottom − modif: modif 0 = закрыто (створка на всю высоту), + * 43 = открыто (остаётся только верхняя кромка), + * 34 верх коробки двери. + * Куски непрозрачные (blitters_0_no_transp) — как в оригинале. */ +static void draw_leveldoor(uint8_t modif, int xh, int dmy) +{ + int x = (xh + 1) * 8; + int ybottom = dmy - 13; + int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */ + int y = ybottom - (int)modif; + /* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО + * тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из + * draw_tile_anim_right, разбирая левого соседа). +8 у palace-уровней + * (tbl_level_type) — на уровне 1 не применяется. */ + pop_leveldoor_right = xh * 8 + 48; + pop_env_b(99, x, ybottom); + if (modif) + pop_env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */ + for (;;) { + pop_env_b(33, x, ylo); + if (y > ylo) ylo += 4; + else break; + } + /* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое + * значение (низ поднятой створки), а не стартовое. */ + pop_leveldoor_ybottom = ylo; + pop_env_b(34, x, dmy - 64); +} + +/* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */ +static void draw_tile(int row, int col) +{ + uint8_t code = pop_tile_code(row, col); + uint8_t lcode = pop_tile_code(row, col - 1); + uint8_t lmod = pop_tile_mod(row, col - 1); + /* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener + * (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05). + * Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого + * соседа (его правая грань). */ + if (code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 1; } + else if (code == 0x06) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 5; } + if (lcode == 0x0F) { if ((pop_doorlink2(lmod) & 0x1F) > 1) { lcode = 1; lmod = 0; } } + else if (lcode == 0x06) { if ((pop_doorlink2(lmod) & 0x1F) > 1) lcode = 5; } + int xh = POP_COL_XH[col]; + int x = xh * 8; + int dby = 63 * row + 65; + int dmy = dby - 3; + const piece *t = &pop_tile_table[code]; + const piece *lt = &pop_tile_table[lcode]; + uint8_t rbl_code; + uint8_t wmod; + + /* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0 + * комнаты снизу (load_rowbelow, seg008:368): row_below_left_[col] = + * room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */ + if (row + 1 > 2) { + int lc = col - 1; + /* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате + * (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда; + * «стена» там стоит, только если такой комнаты нет — как в + * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ + if (lc < 0) rbl_code = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20; + else rbl_code = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 1; + } else rbl_code = pop_tile_code(row + 1, col - 1); + + /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */ + if (code == 0 || code == 9 || code == 12 || code == 26) { + draw_topright(rbl_code, x, dby); + /* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op + * (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет + * floor_right: lt->floor_right. */ + } + + /* --- draw_tile_right (seg008.c:456) --- */ + if (code != 20) { + if (lcode == 20) { + pop_wall_b(1, x, pop_tile_table[20].right_y + dmy); /* грань стены */ + } else if (lcode == 0) { + if (lmod <= 3 && BLUELINE_FRAM1[lmod]) + pop_env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy); + } else if (lcode == 1) { + uint8_t num; + pop_env_b(42, x, pop_tile_table[1].right_y + dmy); /* правый треуг. пола */ + num = (lmod <= 3) ? lmod : 0; + if (num != 0) /* dungeon: level_type=0 */ + pop_env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */ + } else { + if (lt->right_id) + pop_env_b(lt->right_id, x, lt->right_y + dmy); + /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */ + if (lcode == 19 || lcode == 30) + pop_env_b(146, x, dby - 28); + /* stripe — только палас, dungeon пропускает */ + } + } + + /* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для + * верхней полосы (row<0, draw_tile_aboveroom) НЕ рисуется + защита от + * отрицательного индекса в pop_loose_modif. --- */ + if (row >= 0) { + if (lcode == 4) { /* gate: решётка */ + pop_gate_back_b(lmod, xh, dby, dmy); + } else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ + pop_env_b(LOOSE_FRAM_RIGHT[pop_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); + } else if (lcode == 2) { /* spike: статич. кадр */ + uint8_t sf = (lmod & 0x80) ? 5 : lmod; + if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) + pop_env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); + } else if (lcode == 16) { /* дверь уровня */ + draw_leveldoor(lmod, xh, dmy); + } + /* Пламя факела (seg008:560, chtab_1 id modifier+1) в фон НЕ запекаем: + * оно анимируется каждый кадр, а heal вернул бы запечённый кадр из-под + * нового. Рисует pop_torch_draw (динамика, pop_process_trobs). */ + } + + /* --- draw_tile_bottom (seg008.c:570) --- */ + if (code == 20) { + wmod = pop_wall_modifier(row, col); + pop_wall_b(POP_WALL_FRAM_BOTTOM[wmod], x, dby); /* сплошная нижняя грань */ + pop_wall_pattern_b(row, col, 0); + } else if (t->bottom_id) { + pop_env_b(t->bottom_id, x, dby); + } + + /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В + * верхней полосе (row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху): + * её состояние живёт отдельно, в pop_ceil_modif[col] (pop_map). --- */ + if (code == 11) + pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col] + : pop_ceil_modif[col])], x, dby); + + /* --- draw_tile_base (seg008.c:611). Верхняя полоса (row<0) базу НЕ + * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */ + if (row >= 0) { + uint8_t base_id = t->base_id; + if (code == 11) /* loose: левая грань (анимир.) */ + base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])]; + else if (code == 0x0F && lcode == 0) /* opener, слева пусто */ + base_id = 148; /* левая половина без пола */ + if (base_id) + pop_env_b(base_id, x, t->base_y + dmy); + } + + /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в + * СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики + * рисуется только правым соседом со сдвигом → шахта над самой пикой + * пустая. */ + if (row >= 0 && code == 2) { + uint8_t sm = pop_tile_mod(row, col); + uint8_t sf = (sm & 0x80) ? 5 : sm; + if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf]) + pop_env_b(POP_SPIKES_FRAM_LEFT[sf], x, dmy - 2); + } + /* Меч-предмет на полу (тот же draw_tile_anim, case tiles_22_sword): + * chtab_1 (наш pot-атлас), id = (modif==1)+10, низ на draw_main_y-3. + * В оригинале это midtable (рисуется вместе с объектами) — у нас статикой + * в фоне: пока меч лежит, он не анимируется, а Kid и так поверх фона. */ + if (row >= 0 && code == 22) + pop_pot_b((uint8_t)((pop_tile_mod(row, col) == 1) + 10), x, dmy - 3); + + /* --- draw_tile_fore (seg008.c:690) --- */ + if (code == 20) { + wmod = pop_wall_modifier(row, col); + pop_wall_b(POP_WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ + pop_wall_pattern_b(row, col, 1); + } + if (code == 10) { /* зелье: chtab_1, не env fore_id. + * Пузырёк — динамика (pop_potion_draw). */ + pop_potion_flask(row, col, pop_tile_mod(row, col)); + } else if (t->fore_id) { + pop_fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); + } +} + + +/* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- * + * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать + * ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif + * внутри draw_tile). */ +void pop_loose_shake_draw(int row, int col) __banked +{ + int x = POP_COL_XH[col] * 8; + /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх — + * перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из + * pop_loose_modif). Порт redraw_tile_height (loose + правый сосед). + * NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое + * решение (bake тайла как empty + рисовать loose всегда динамически) + * отложено; wipe-в-чёрный делал плиту невидимой — не годится. */ + pop_heal_off(x, 63 * row + 46, 64, 24); + gfx_set_bank(GFX_BANK_SPRITE); + draw_tile(row, col); /* loose: лево+низ (анимир.) */ + if (col + 1 < 10) + draw_tile(row, col + 1); /* сосед: правая грань loose (анимир.) + его пол */ + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif + * (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2 + * читает lmod = pop_t_bg[row*10+col] = живой modif пики). pop_t_bg должен указывать + * на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile. + * heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */ +void pop_spike_redraw(int row, int col) __banked +{ + int x = POP_COL_XH[col] * 8; + /* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ + * redraw ДВУХ тайлов — своего и правого соседа (остриё рисуется в его + * ячейке). Именно ПОЛНЫЙ: рисовать только два спрайта остриёв нельзя — + * порядок слоёв внутри тайла (draw_tile_anim_right идёт ПЕРВЫМ, база и + * fore соседа ложатся ПОВЕРХ) тогда теряется, и остриё лезет на колонну + * справа. Заодно draw_tile сам восстанавливает вклад соседних пик: + * в ячейке col — правое остриё пики col-1 (ветка lcode==2). + * Шипы выдвигаются выше линии пола (dmy-7), поэтому heal с запасом вверх + * (63*row+34 .. +74); ширина 64 = обе ячейки. */ + pop_heal_off(x, 63 * row + 34, 64, 40); + gfx_set_bank(GFX_BANK_SPRITE); + draw_tile(row, col); + if (col + 1 < 10) + draw_tile(row, col + 1); + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Перерисовать ЛЕВЫЙ шов (col0) на back-странице по живому modif соседа — + * ворота соседней комнаты слева (portcullis room8) видны в col0 текущей + * комнаты через draw_gate_back (draw_tile ветка lcode==4, lmod = pop_t_lbg[0]). + * pop_t_lbg (lcol_bg) должен указывать на живой modif (roomtest обновляет из + * room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake): + * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары + * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ +void pop_room_redraw_seam_left(void) __banked +{ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + /* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2, + * а бары ворот начинаются с y=3 (gate_top_y = dby−62). Раньше bar шёл от + * POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать ещё и + * draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации решётки. */ + bar(0, POP_YOFF + 3, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(0, 0); /* фон col0 + бары ворот (живой modif) */ + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- * + * В полосе кладки у потолка (наш ряд −1) виден только НИЗ плиты + * (draw_tile_aboveroom: bottom+loose, всё выше отрезано клипом POP_YOFF). + * Дрожащий кадр берётся из pop_ceil_modif[col] (см. draw_loose выше). + * shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту + * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */ + +void pop_ceil_shake_draw(int col) __banked +{ + int x = POP_COL_XH[col] * 8; + pop_heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ + gfx_set_bank(GFX_BANK_SPRITE); + pop_t_clip_top = POP_YOFF; + draw_tile(-1, col); + if (col + 1 < 10) draw_tile(-1, col + 1); + pop_t_clip_top = 0; + gfx_set_bank(GFX_BANK_NORMAL); +} + +void pop_ceil_bake_empty(int col) __banked +{ + int x = POP_COL_XH[col] * 8; + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H); /* стереть плиту в ОЗУ-копии */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + pop_t_clip_top = POP_YOFF; + pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ + /* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только + * два тайла ряда −1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей + * нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут: + * • ряд −1, колонки col−1..col+1 — сама полоса кладки у потолка; + * • ряд 0, колонки col−1..col+1 — их верхушки заходят в те же 8 px. + * Раньше перерисовывались только (−1,col) и (−1,col+1), и в полосе + * оставался чёрный прямоугольник ~13×3 px (замер: экранные y 47..52 при + * полосе 44..52), который heal возвращал каждый кадр. Признак, что дело + * именно в запечке: после выхода из комнаты и возврата бара нет. */ + { + int c0 = (col > 0) ? col - 1 : 0; + int c1 = (col + 1 < 9) ? col + 1 : 9; + int c; + for (c = c0; c <= c1; c++) draw_tile(-1, c); + for (c = c0; c <= c1; c++) draw_tile(0, c); + } + pop_t_bake_rest = 0; + pop_t_clip_top = 0; + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* ---- Анимация факела и зелья (динамика, каждый кадр) ---------------- * + * Оба кадра рисуются ПОВЕРХ статического фона и стираются heal'ом своей + * области (фон запечён БЕЗ них). Факел: пламя chtab_1 id modifier+1 в + * ячейке ПРАВОГО соседа (seg008:560: xh = draw_xh + 1, ybottom = + * draw_main_y − 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька + * potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y − + * (pot_size<<2) − 14 (seg008:668). */ +void pop_torch_draw(int row, int col, uint8_t modif) __banked +{ + /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = POP_COL_XH[col+1] + 1 + * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env + * 146), которое тоже лежит в ячейке соседа. */ + int x = (col + 1 < 10 ? POP_COL_XH[col + 1] : POP_COL_XH[col] + 4) * 8 + 8; + int yb = 63 * row + 62 - 40; + if (modif >= 9) return; + /* heal НЕ нужен: оригинал рисует пламя blitters_0_no_transp (seg008 + * draw_tile_anim_right), и все 9 кадров лежат на общем канвасе 16x18 — + * непрозрачный блит сам полностью накрывает предыдущий кадр. Кадры + * упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS). */ + gfx_set_bank(GFX_BANK_SPRITE); + pop_pot_b((uint8_t)(modif + 1), x, yb); + gfx_set_bank(GFX_BANK_NORMAL); +} + +void pop_potion_draw(int row, int col, uint8_t modif) __banked +{ + uint8_t type = potion_type(modif); + int pot_size = (type >= 2 && type < 5) ? 1 : 0; + int x = (POP_COL_XH[col] + 3) * 8 + 1; + int yb = 63 * row + 62 - (pot_size << 2) - 14; + if (type == 0) return; + pop_heal_off(x - 1, yb - 10, 8, 12); + gfx_set_bank(GFX_BANK_SPRITE); + pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ + pop_pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая + * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + + * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе + * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ +void pop_floor_bake(int row, int col) __banked +{ + int x = POP_COL_XH[col] * 8; + int yb = POP_YOFF + 63 * row; + /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. + * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже + * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, yb + 26, x + 59, yb + 64); + gfx_set_bank(GFX_BANK_TRANSPARENT); + pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ + draw_tile(row, col); + if (col + 1 < 10) draw_tile(row, col + 1); + pop_t_bake_rest = 0; + gfx_set_bank(GFX_BANK_NORMAL); +} + +void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); } + +/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка, + * см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет, + * а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое. + * Куски двери непрозрачные — wipe не нужен. */ +void pop_leveldoor_redraw(int row, int col) __banked +{ + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(row, col); + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob, + * seg007:01E6 — им заканчивается animate_door). (row,col) — сами ворота; + * решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому + * wipe и redraw идут по ней. Логика ровно как у левого шва + * (pop_room_redraw_seam_left), только колонка произвольная. + * + * Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при + * ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары. Полоса — ровно + * зона решётки: от gate_top_y (dby−62 = 63*row+3) до низа (63*row+64); + * выше начинается ряд −1, ниже — стена ряда ниже. + * + * Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их + * не видно — оригинал такой trob тоже пропускает + * (get_trob_right_pos_in_drawn_room → 30). Для взгляда из комнаты справа + * работает pop_room_redraw_seam_left. */ +void pop_gate_redraw(int row, int col) __banked +{ + int x, yt; + if (col + 1 > 9 || row < 0 || row > 2) return; + x = POP_COL_XH[col + 1] * 8; + yt = POP_YOFF + 63 * row; + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, yt + 3, x + 31, yt + 64); + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(row, col + 1); + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт + * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И + * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — + * рисовал loose-правый-край pop_env_b(42); теперь пусто → чистая грань). + * pop_t_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые + * пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */ +void pop_loose_bake_empty(int row, int col) __banked +{ + int x = POP_COL_XH[col] * 8; + int xr = POP_COL_XH[col + 1] * 8; + int yb = 63 * row + 46; + /* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора): + * стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе + * heal вернул бы старый пол из ОЗУ-копии. Ширина 40 — кусок пола + * заходит на x+32..+40. */ + /* НИЗ строго 63*row+65 = низ собственной графики плиты (bottom_id 43/73/74 + * стоит ровно на dby; base 41/69/70 — до dmy=62; правая грань 42/66/67 — + * до dby-1). Ниже начинается СТЕНА ряда ниже (высокий спрайт), которую + * здесь не перерисовываем: заливка на 9 строк глубже оставляла ЧЁРНЫЙ БОКС + * под соседним столбом, причём в ОЗУ-копии — то есть heal возвращал его + * снова, и на второй странице дабл-буфера его не было → мерцание. + * Тот же урок уже учтён в pop_floor_bake. */ + yb += POP_YOFF; + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, yb, x + 39, POP_YOFF + 63 * row + 65); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */ + if (col + 1 < 10) + bar(xr, yb, xr + 31, POP_YOFF + 63 * row + 65); /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */ + /* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты + * снизу) + правый сосед (его левая грань: был loose-край pop_env_b(42), + * теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в + * ОЗУ-копию → heal сохранит. */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ + draw_tile(row, col); + if (col + 1 < 10) + draw_tile(row, col + 1); + pop_t_bake_rest = 0; + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* ---- Падающий кусок loose (mob, seg007 move_loose) ------------------ * + * Упрощ. одиночный кусок (в комнате 1 — один loose). Модель fixed- + * corridor: коридор падения (колонка тайла) heal'ится каждый кадр (стирает + * прошлый кусок, возвращает печёный фон — empty+«стена вниз»), поверх — + * кадр 0 loose (41/42/43) на текущей y. Гравитация: speed 0→+3/кадр + * (cap 29), y+=speed. Двоебуфер: коридор фиксирован → heal чистит любой + * прошлый кадр ЭТОЙ страницы. Ниже поля кусок прячет clip_borders. */ +/* Падающие куски — ТАБЛИЦА (порт mobs[14]/mobs_count, seg007:965). Одного + * куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе + * подряд, и второй спавн затирал состояние первого — его пиксели оставались + * висеть в воздухе (на каждой странице на своей y → мерцание), а щебень + * появлялся только под одной плитой. 4 слота: больше одновременно падающих + * плит в одной комнате практически не бывает (в оригинале 14). */ +#define MOB_MAX 4 +typedef struct { + uint8_t active; /* 0 нет; 1 падает */ + uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */ + uint8_t defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */ + int x; /* левый край (POP_COL_XH*8) */ + int y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */ + int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ + int8_t speed; + int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ + uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */ +} mob_t; +static mob_t mobs[MOB_MAX]; + +/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на + * который он лёг → приложение/pop_map ставит debris + do_knock. */ +/* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а + * писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */ + +/* y_loose_land / y_something (seg007): [row+1], row = −1..3. Ряд −1 — + * плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный + * tilepos, у нас — свой ряд полосы у потолка. */ +static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254}; +static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25}; +#define MOB_Y_NONE 0x7FFF + +void pop_loose_mob_spawn(int row, int col) __banked +{ + uint8_t i; + mob_t *m = 0; + /* свободный слот = не летит И дочистка завершена (иначе затрём хвост, + * который ещё не стёрт со второй страницы) */ + for (i = 0; i < MOB_MAX; i++) + if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; } + if (!m) return; /* таблица полна — кусок без спрайта (тайл всё равно пуст) */ + m->x = POP_COL_XH[col] * 8; + m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */ + m->row = (int8_t)row; + m->col = (int8_t)col; + m->room = pop_t_room; /* curmob.room (add_mob, seg007:1010) */ + m->speed = 0; + m->active = 1; + m->clean = 0; + m->defer = 0; + m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; +} + +/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26) + * с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46: + * ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину + * [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной + * странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */ +#define MOB_X0(mx) ((mx) - 4) +#define MOB_W 64 /* mob_x-4 .. mob_x+59 (крышка +57 с запасом 2px) */ + +/* Остановить падающий кусок (при смене комнаты — состояние per-tile не + * должно утечь в новую комнату). */ +static void mob_render(mob_t *m, uint8_t pg); + +void pop_loose_mob_reset(void) __banked +{ + uint8_t i; + for (i = 0; i < MOB_MAX; i++) { + mobs[i].active = 0; mobs[i].clean = 0; mobs[i].defer = 0; + mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; + } +} + +/* Смена комнаты. Куски НЕ останавливаем: в оригинале mobs[] чистится только + * в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо + * от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид + * не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12 + * плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только + * heal-историю: новая комната перерисована целиком, старых пикселей нет. */ +void pop_loose_mob_room_changed(void) __banked +{ + uint8_t i; + for (i = 0; i < MOB_MAX; i++) { + mobs[i].defer = 0; + mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; + /* дочистка хвоста относилась к покинутой комнате — она уже не нужна */ + if (!mobs[i].active) mobs[i].clean = 0; + } +} + +/* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки + * и соседей), в чужой — прямо из данных уровня. */ +static uint8_t mob_tile_at(mob_t *m) +{ + if (m->room == pop_t_room) return (uint8_t)(pop_tile_code(m->row, m->col) & 0x1F); + if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1; /* кромка = пол */ + return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F); +} + +static void mob_tick_one(mob_t *m, uint8_t pg) +{ + uint8_t here; + if (!m->active && !m->clean) return; + here = (uint8_t)(m->room == pop_t_room); + /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой + * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; + * плита-потолок падает через весь ряд 0, поэтому чистим по фактической + * прошлой позиции. */ + if (here && m->prev_y[pg] != MOB_Y_NONE) { + pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); + m->prev_y[pg] = MOB_Y_NONE; + } + if (!m->active) { m->clean--; return; } + /* гравитация (move_loose): ускорение до 29 */ + if (m->speed < 29) m->speed += 3; + m->y += m->speed; + /* move_loose (seg007:1126): низ куска пересёк границу своего ряда — + * смотрим тайл этого ряда. Пусто/loose — валится дальше (порт + * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); + * пол — приземление (loose_land: debris + do_knock). */ + if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { + uint8_t t = mob_tile_at(m); + if (t == 0 || t == 11) { + m->row++; /* mob_down_a_row */ + } else if (!here) { + /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, сигнальная + * пара pop_loose_landed/pop_debris_at обслуживает только текущую + * комнату — щебень кладём прямо в данные уровня (loose_land пишет + * в curr_room_tiles комнаты curmob.room, а не отрисованной). */ + pop_level_set_tile(m->room, (uint8_t)(m->row * 10 + m->col), 0x0E); + m->active = 0; m->clean = 0; + return; + } else if (pop_loose_landed) { + /* сигнал прошлого приземления ещё не разобран (один за кадр) — + * подержим кусок на границе и приземлим следующим кадром */ + m->y = MOB_Y_BOUND[m->row + 1]; + } else { + m->y = MOB_Y_BOUND[m->row + 1]; + pop_loose_landed = (uint8_t)(m->row * 10 + m->col + 1); + m->active = 0; m->clean = 2; /* кусок исчез — на его месте debris */ + return; + } + } + if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */ + m->active = 0; m->clean = (uint8_t)(here ? 2 : 0); + return; + } + if (!here) return; /* чужая комната — считаем, но не рисуем */ + /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ + * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри + * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто + * ниже — тот позже, т.е. поверх). Тайл падающего куска — draw_mob + * (seg007:13E5): row = y_to_row_mod4(y), col = xh>>2. Значит кусок + * поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при + * совпадении тайлов — если он ниже по y. */ + { + int8_t mrow = (int8_t)(((m->y + 60) / 63) % 4 - 1); /* y_to_row_mod4 */ + uint8_t over; + if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */ + else if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col); + else over = (m->y > pop_cd[POP_CH_KID].fpy); + if (over) { m->defer = 1; return; } + } + mob_render(m, pg); +} + +void pop_loose_mob_tick(void) __banked +{ + uint8_t pg = gfx_get_draw_page() & 1, i; + for (i = 0; i < MOB_MAX; i++) + mob_tick_one(&mobs[i], pg); +} + +static void mob_render(mob_t *m, uint8_t pg) +{ + /* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска — + * left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1, + * заходит в тайл СПРАВА) на obj_y-1. */ + gfx_set_bank(GFX_BANK_SPRITE); + pop_env_b(43, m->x, m->y); + pop_env_b(41, m->x, m->y - 3); + pop_env_b(42, m->x + 32, m->y - 1); + m->prev_y[pg] = m->y; + /* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob: + * set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob). + * Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола, + * её правый край ПРЯЧЕТСЯ за передней гранью пола. Перерисовываем + * сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу + * до heal'а следующего кадра — виден как хвост во время падения). */ + if (m->col + 1 < 10) { + /* Ряд соседа берём ИЗ КООРДИНАТЫ куска (y_to_row_mod4(ypos)), а НЕ из + * m->row: последний — логический счётчик «сквозь какой ряд летим», и + * mob_down_a_row уводит его на ряд ВПЕРЁД (для плиты нижнего ряда — + * сразу в 3, которого нет). Тогда пол соседа не перерисовывался + * вообще, и правый кусок 42 оставался ПОВЕРХ него: грань плиты видна + * через пол + перекрыта передняя грань пола. + * Плюс ВТОРОЙ тайл — под ВЕРХОМ куска (ypos-18), пока кусок висит на + * границе рядов. Порт draw_mob (seg007:13E5). */ + int8_t r = (int8_t)(((m->y + 60) / 63) % 4 - 1); + int8_t rt = (int8_t)(((m->y - 18 + 60) / 63) % 4 - 1); + if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */ + draw_tile(r, m->col + 1); + pop_t_clip_top = 0; + if (rt != r) { + if (rt < 0) pop_t_clip_top = POP_YOFF; + draw_tile(rt, m->col + 1); + pop_t_clip_top = 0; + } + } + gfx_set_bank(GFX_BANK_NORMAL); + /* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */ + pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30); +} + +/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать + * после отрисовки персонажей, до fore-слоя. */ +void pop_loose_mob_draw_over(void) __banked +{ + uint8_t pg = gfx_get_draw_page() & 1, i; + for (i = 0; i < MOB_MAX; i++) + if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); } +} + +/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и + * y (низ куска). 0 — куска нет. */ +uint8_t pop_loose_mob_pos(int *col, int *y) __banked +{ + uint8_t i; + /* check_loose_fall_on_kid (seg007:1192) начинается с Char.room == + * curmob.room: кусок, падающий в покинутой комнате, Киду по голове не + * бьёт. */ + for (i = 0; i < MOB_MAX; i++) + if (mobs[i].active && mobs[i].room == pop_t_room) { + *col = mobs[i].col; *y = mobs[i].y; return 1; + } + return 0; +} + +/* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл- + * буфера). Узкий per-спрайт heal НЕ используем: accel-выравнивание блока + * оставляет 1–2px слайверы правой грани → на одной странице мерцает. + * Вместо этого борта чистит pop_room_clip_borders (полоса, но ТОЛЬКО в + * кадрах падения — гейт по флагу). */ +static uint8_t border_dirty; + +void pop_clip_sprite(int x, int w, int sy_top, int h) __banked +{ + int sy_bot = sy_top + h; + (void)x; (void)w; + if (sy_bot > POP_CLIP_BOTTOM || sy_top < POP_YOFF) + border_dirty = 2; /* 2 кадра = обе страницы */ +} + +/* Клип бортов (см. pop_bg.h): работает ТОЛЬКО пока border_dirty (кто-то + * вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает + * чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу + * + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */ +void pop_room_clip_borders(void) __banked +{ + if (!border_dirty) return; + border_dirty--; + /* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320 + * не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска + * по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах + * падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там + * тонет в цене самих 320×28 пикселей. Не «оптимизировать» повторно. */ + gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */ + gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */ +} + +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) __banked +{ + int row, col; + pop_t_room = room_num; + pop_wall_pattern_reset_b(); /* кэш кладки привязан к номеру комнаты */ + pop_t_fg = fg; pop_t_bg = bg; + pop_t_lfg = lcol_fg; pop_t_lbg = lcol_bg; + + /* прозрачный блит: 0xFF не пишется (чёрный фон/нижние слои проступают) */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ + for (row = 2; row >= 0; row--) /* снизу вверх (2,1,0) */ + for (col = 0; col < 10; col++) + draw_tile(row, col); + /* Полоса кладки у потолка (seg008 draw_room: доп. ряд из room_A, row2 + * при draw_main_y=-1): нижние/правые грани ряда 2 комнаты сверху. Кадры + * высокие — верх ушёл бы в борт (за поле); клип по верхней границе поля + * (POP_YOFF) рисует ТОЛЬКО срез у потолка, без зачернения борта. */ + pop_t_clip_top = POP_YOFF; + for (col = 0; col < 10; col++) + draw_tile(-1, col); + pop_t_clip_top = 0; + pop_t_bake_rest = 0; + gfx_set_bank(GFX_BANK_NORMAL); +} \ No newline at end of file diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index fc2190c..dbb95d6 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -50,19 +50,21 @@ __sfr __at (0xFE) io_border; #endif /* Сколько банков грузит crt0_banked (читает его стартовый код). - * 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map - * (коллизия/физика), 4 = pop_cdraw (отрисовка персонажей + полоса HP), - * 5 = pop_ctrl (диспетчер управления). - * Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ - * вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних - * уровней, чомперы, зеркало. + * 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход, + * оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw + * (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления), + * 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина + * фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты). + * Отрисовка персонажей и холодная половина фона вынесены из банка 2 + * отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть + * куда — тайлы поздних уровней, чомперы, зеркало. * * ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть — это не ошибка * сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и * останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку * runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра * доходила до конца загрузки ресурсов и вставала намертво в DSS. */ -const uint8_t n_banks = 6; +const uint8_t n_banks = 7; /* Режим читов (pop_cheat.h): на время разработки включаем в main. */ uint8_t pop_cheats;