MEM-BANK2 шаг 3: холодная половина слоя фона в банк 7 (90.4% -> 35.8%)

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 16:28:02 +03:00
parent bbf91d10ee
commit 14190f0210
9 changed files with 1116 additions and 929 deletions
+22 -11
View File
@@ -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`, отложен по
+3 -1
View File
@@ -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`) поверх `<kbd_raw.h>`. |
+2 -2
View File
@@ -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)
+117
View File
@@ -322,6 +322,123 @@ BANK4 pop_cdraw 4 625 5 905 +1 280 (сюда переехала отрис
<a id="clip-1"></a>
### <a id="mem-bank2"></a>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 Кида и стража переведены на выбор ядра по тому же тесту,
+37 -71
View File
@@ -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 @@
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
живой страж — переход не происходит.
### <a id="mem-bank2"></a>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`-
обёртка (тело остаётся непомеченным, горячие вызывающие платят ноль).
### <a id="l3-chomp"></a>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)).
### <a id="draw-cost"></a>DRAW-COST. Стоимость fore-прохода после DRAW-CHAR
### <a id="draw-cost"></a>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), а он УЖЕ спрайта; расширение
+49
View File
@@ -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 <stdint.h>
/* Состояние дрожащих плит — живёт в 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
+31 -837
View File
@@ -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 <graphics.h>
#include <gfx.h>
@@ -14,6 +14,7 @@
#include <fcntl.h>
#include <unistd.h>
#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 = dby62). Раньше 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 (dby62 = 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) —
+846
View File
@@ -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 <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <fcntl.h>
#include <unistd.h>
#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 = dby62). Раньше 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 (dby62 = 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);
}
+9 -7
View File
@@ -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;